diff --git a/.gitignore b/.gitignore index 675bd9392..3a85576e6 100644 --- a/.gitignore +++ b/.gitignore @@ -33,10 +33,8 @@ /build/package/verify/.rnd /deploy/.env /log/* -/test/node_modules -/test/config/.overrides.yml -/test/log/* -/test/tmp/* +/test/reports +/test/tmp /ci_cache /vendor /website/build diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 000000000..1dacaeee7 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,182 @@ +AllCops: + TargetRubyVersion: 2.3 + Exclude: + - bin/**/* + - tmp/**/* + - vendor/**/* + +Rails: + Enabled: true + +Lint/AssignmentInCondition: + AllowSafeAssignment: false + +Lint/LiteralInInterpolation: + Enabled: false + +Lint/UnusedBlockArgument: + Enabled: false + +Lint/UnusedMethodArgument: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Metrics/BlockNesting: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/LineLength: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Performance/RedundantMerge: + Enabled: false + +Style/AlignHash: + Enabled: false + +Style/AlignParameters: + Enabled: false + +Style/AsciiComments: + Enabled: false + +Style/BracesAroundHashParameters: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Style/ClassCheck: + Enabled: false + +Style/CollectionMethods: + PreferredMethods: + find: 'detect' + +Style/ConditionalAssignment: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/EachWithObject: + Enabled: false + +Style/EmptyElse: + EnforcedStyle: empty + +Style/EmptyMethod: + Enabled: false + +Style/Encoding: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/GuardClause: + Enabled: false + +Style/HashSyntax: + EnforcedStyle: hash_rockets + +Style/IfInsideElse: + Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Style/IndentArray: + EnforcedStyle: consistent + +Style/IndentHash: + EnforcedStyle: consistent + +Style/Lambda: + Enabled: false + +Style/NegatedIf: + Enabled: false + +Style/Next: + Enabled: false + +Style/NumericLiterals: + Enabled: false + +Style/NumericPredicate: + Enabled: false + +Style/OneLineConditional: + Enabled: false + +Style/ParenthesesAroundCondition: + Enabled: false + +Style/RaiseArgs: + Enabled: false + +Style/RedundantSelf: + Enabled: false + +Style/RegexpLiteral: + Enabled: false + +Style/SignalException: + Enabled: false + +Style/SpaceAroundKeyword: + Enabled: false + +Style/StringLiterals: + Enabled: false + +Style/StringLiteralsInInterpolation: + Enabled: false + +Style/SymbolProc: + Enabled: false + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInLiteral: + EnforcedStyleForMultiline: comma + +Style/WhileUntilModifier: + Enabled: false + +Style/WordArray: + Enabled: false + +Rails/Delegate: + Enabled: false + +Rails/FindBy: + Enabled: false + +Rails/HasAndBelongsToMany: + Enabled: false + +Rails/Output: + Exclude: + - db/migrate/** diff --git a/Berksfile.lock b/Berksfile.lock index fd0dbeba2..252663c05 100644 --- a/Berksfile.lock +++ b/Berksfile.lock @@ -1,49 +1,31 @@ DEPENDENCIES api-umbrella git: https://github.com/NREL/api-umbrella-cookbook.git - revision: 46e8637cdd591abfa74aef5c9ffbcda69ff888a5 + revision: 04238ed07c24af20b0de0603cb076c002b5b2baa GRAPH api-umbrella (0.7.1) build-essential (>= 0.0.0) - nodejs (>= 0.0.0) - phantomjs (>= 0.0.0) sudo (>= 0.0.0) ulimit (>= 0.0.0) yum (>= 0.0.0) yum-epel (>= 0.0.0) - apt (4.0.2) - compat_resource (>= 12.10) - ark (2.0.0) - build-essential (>= 0.0.0) - seven_zip (>= 0.0.0) - windows (>= 0.0.0) - build-essential (6.0.5) + build-essential (7.0.2) compat_resource (>= 12.14) mingw (>= 1.1) seven_zip (>= 0.0.0) - chocolatey (1.0.3) - windows (>= 1.38) - compat_resource (12.14.3) - homebrew (2.1.2) - build-essential (>= 2.1.2) + compat_resource (12.16.2) mingw (1.2.4) compat_resource (>= 0.0.0) seven_zip (>= 0.0.0) - nodejs (2.4.4) - apt (>= 0.0.0) - ark (>= 0.0.0) - build-essential (>= 0.0.0) - homebrew (>= 0.0.0) - yum-epel (>= 0.0.0) - phantomjs (1.0.3) - build-essential (>= 0.0.0) - chocolatey (>= 0.0.0) + ohai (4.2.2) + compat_resource (>= 12.14) seven_zip (2.0.2) windows (>= 1.2.2) - sudo (3.0.0) + sudo (3.1.0) ulimit (0.4.0) - windows (2.0.2) - yum (4.0.0) - yum-epel (1.0.1) - yum (>= 3.6) + windows (2.1.1) + ohai (>= 4.0.0) + yum (4.1.0) + yum-epel (2.0.0) + compat_resource (>= 12.14.6) diff --git a/CMakeLists.txt b/CMakeLists.txt index dd1d3ea67..f94bb707c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,10 @@ set(VENDOR_DIR ${WORK_DIR}/vendor) set(VENDOR_LUA_DIR ${VENDOR_DIR}/share/lua/5.1) set(LUAROCKS_CMD env LUA_PATH=${STAGE_EMBEDDED_DIR}/openresty/luajit/share/lua/5.1/?.lua$${STAGE_EMBEDDED_DIR}/openresty/luajit/share/lua/5.1/?/init.lua$$ ${STAGE_EMBEDDED_DIR}/bin/luarocks) +# Where to install development-only dependencies. +set(DEV_INSTALL_PREFIX ${WORK_DIR}/dev-env) +set(DEV_VENDOR_DIR ${DEV_INSTALL_PREFIX}/vendor) + # Where to install test-only dependencies. set(TEST_INSTALL_PREFIX ${WORK_DIR}/test-env) set(TEST_VENDOR_DIR ${TEST_INSTALL_PREFIX}/vendor) @@ -59,6 +63,7 @@ pkg_search_module(LIBFFI REQUIRED libffi) require_program(rsync) include(${CMAKE_SOURCE_DIR}/build/cmake/versions.cmake) +include(${CMAKE_SOURCE_DIR}/build/cmake/dev/nodejs.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/elasticsearch.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/libcidr.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/libgeoip.cmake) @@ -72,8 +77,6 @@ include(${CMAKE_SOURCE_DIR}/build/cmake/ruby.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/rsyslog.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/trafficserver.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/static-site.cmake) -include(${CMAKE_SOURCE_DIR}/build/cmake/core-lua-deps.cmake) -include(${CMAKE_SOURCE_DIR}/build/cmake/core-web-app.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/core.cmake) if(ENABLE_HADOOP_ANALYTICS) include(${CMAKE_SOURCE_DIR}/build/cmake/hadoop-analytics/flume.cmake) @@ -86,14 +89,14 @@ endif() # Testing # if(ENABLE_TEST_DEPENDENCIES) - include(${CMAKE_SOURCE_DIR}/build/cmake/test/unbound.cmake) - include(${CMAKE_SOURCE_DIR}/build/cmake/test/mongo-orchestration.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/lua-deps.cmake) + include(${CMAKE_SOURCE_DIR}/build/cmake/test/mailhog.cmake) + include(${CMAKE_SOURCE_DIR}/build/cmake/test/mongo-orchestration.cmake) + include(${CMAKE_SOURCE_DIR}/build/cmake/test/phantomjs.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/shellcheck.cmake) + include(${CMAKE_SOURCE_DIR}/build/cmake/test/unbound.cmake) endif() -include(${CMAKE_SOURCE_DIR}/build/cmake/test/lint.cmake) -include(${CMAKE_SOURCE_DIR}/build/cmake/test/test-proxy.cmake) -include(${CMAKE_SOURCE_DIR}/build/cmake/test/test-web-app.cmake) +include(${CMAKE_SOURCE_DIR}/build/cmake/test/bundle.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/test.cmake) # @@ -111,8 +114,3 @@ include(${CMAKE_SOURCE_DIR}/build/cmake/package.cmake) # include(${CMAKE_SOURCE_DIR}/build/cmake/clean-download-archives.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/distclean.cmake) - -# -# Outdated Dependencies Check -# -include(${CMAKE_SOURCE_DIR}/build/cmake/outdated.cmake) diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..41bc6e161 --- /dev/null +++ b/Gemfile @@ -0,0 +1,84 @@ +source "https://rubygems.org" + +gem "rake", "~> 11.3.0" + +# Tests +gem "minitest", "~> 5.10.1" + +# More test outputs +gem "minitest-reporters", "~> 1.1.13" + +# For an "after_all" callback. +gem "minitest-hooks", "~> 1.4.0" + +# Test metadata for CI environment. +gem "minitest-ci", "~> 3.1.0" + +# Ruby lint/style checker +gem "rubocop", "~> 0.46.0", :require => false + +# Running background processes +gem "childprocess", "~> 0.5.9" + +# Making HTTP requests +gem "typhoeus", "~> 1.1.2" + +# JSON parsing +gem "multi_json", "~> 1.12.1" +gem "oj", "~> 2.18.0" + +# Database libraries +gem "mongoid", "~> 6.0.3" +gem "elasticsearch", "~> 2.0.0" +gem "elasticsearch-persistence", "~> 0.1.9" + +# Factories for test database data +gem "factory_girl", "~> 4.7.0" + +# Deleting database data between tests. +gem "database_cleaner", "~> 1.5.3" + +# Programmatically generate Rails session cookies. +gem "rails_compatible_cookies_utils", "~> 0.1.0" + +# Localization tests +gem "i18n", "~> 0.7.0" + +# URL parsing/generation +gem "addressable", "~> 2.5.0" + +# Browser/JavaScript integration tests +gem "capybara", "~> 2.10.2" +# Use fork to fix failure messages: +# https://github.com/wojtekmach/minitest-capybara/pull/17 +gem "minitest-capybara", "~> 0.8.2", :git => "https://github.com/GUI/minitest-capybara.git" + +# Webkit-based driver for capybara +gem "poltergeist", "~> 1.11.0" + +# Take screenshots on capybara test failures +gem "capybara-screenshot", "~> 1.0.14" + +# HTML or XML parsing +gem "nokogiri", "~> 1.6.8" + +# Useful additions +gem "activesupport", "~> 5.0.0" + +# Path-based setting of hashes +gem "lazyhash", "~> 0.1.1" + +# Generating fake strings and data. +gem "faker", "~> 1.6.6" + +# Concurrency helpers. +gem "concurrent-ruby", "~> 1.0.2" + +# Time zone randomization for tests. +gem "zonebie", "~> 0.6.1" + +# Color output +gem "rainbow", "~> 2.1.0" + +# Debug printing +gem "awesome_print", "~> 1.7.0" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..e40522bcd --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,183 @@ +GIT + remote: https://github.com/GUI/minitest-capybara.git + revision: 74743d1472891da8636d773557297495cf8d6a77 + specs: + minitest-capybara (0.8.2) + capybara (~> 2.2) + minitest (~> 5.0) + rake + +GEM + remote: https://rubygems.org/ + specs: + activemodel (5.0.0.1) + activesupport (= 5.0.0.1) + activesupport (5.0.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + ansi (1.5.0) + ast (2.3.0) + awesome_print (1.7.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + bson (4.2.0) + builder (3.2.2) + capybara (2.10.2) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + capybara-screenshot (1.0.14) + capybara (>= 1.0, < 3) + launchy + childprocess (0.5.9) + ffi (~> 1.0, >= 1.0.11) + cliver (0.3.2) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.0.2) + database_cleaner (1.5.3) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + elasticsearch (2.0.0) + elasticsearch-api (= 2.0.0) + elasticsearch-transport (= 2.0.0) + elasticsearch-api (2.0.0) + multi_json + elasticsearch-model (0.1.9) + activesupport (> 3) + elasticsearch (> 0.4) + hashie + elasticsearch-persistence (0.1.9) + activemodel (> 3) + activesupport (> 3) + elasticsearch (> 0.4) + elasticsearch-model (>= 0.1) + hashie + virtus + elasticsearch-transport (2.0.0) + faraday + multi_json + equalizer (0.0.11) + ethon (0.10.1) + ffi (>= 1.3.0) + factory_girl (4.7.0) + activesupport (>= 3.0.0) + faker (1.6.6) + i18n (~> 0.5) + faraday (0.10.0) + multipart-post (>= 1.2, < 3) + ffi (1.9.14) + hashie (3.4.6) + i18n (0.7.0) + ice_nine (0.11.2) + launchy (2.4.3) + addressable (~> 2.3) + lazyhash (0.1.1) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mini_portile2 (2.1.0) + minitest (5.10.1) + minitest-ci (3.1.0) + minitest (~> 5.0, >= 5.0.6) + minitest-hooks (1.4.0) + minitest-reporters (1.1.13) + ansi + builder + minitest (>= 5.0) + ruby-progressbar + mongo (2.4.0) + bson (~> 4.2.0) + mongoid (6.0.3) + activemodel (~> 5.0) + mongo (~> 2.3) + multi_json (1.12.1) + multipart-post (2.0.0) + nokogiri (1.6.8.1) + mini_portile2 (~> 2.1.0) + oj (2.18.0) + parser (2.3.3.1) + ast (~> 2.2) + poltergeist (1.11.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + websocket-driver (>= 0.2.0) + powerpack (0.1.1) + public_suffix (2.0.4) + rack (2.0.1) + rack-test (0.6.3) + rack (>= 1.0) + rails_compatible_cookies_utils (0.1.0) + rainbow (2.1.0) + rake (11.3.0) + rubocop (0.46.0) + parser (>= 2.3.1.1, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + ruby-progressbar (1.8.1) + thread_safe (0.3.5) + typhoeus (1.1.2) + ethon (>= 0.9.0) + tzinfo (1.2.2) + thread_safe (~> 0.1) + unicode-display_width (1.1.1) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) + websocket-driver (0.6.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + xpath (2.0.0) + nokogiri (~> 1.3) + zonebie (0.6.1) + +PLATFORMS + ruby + +DEPENDENCIES + activesupport (~> 5.0.0) + addressable (~> 2.5.0) + awesome_print (~> 1.7.0) + capybara (~> 2.10.2) + capybara-screenshot (~> 1.0.14) + childprocess (~> 0.5.9) + concurrent-ruby (~> 1.0.2) + database_cleaner (~> 1.5.3) + elasticsearch (~> 2.0.0) + elasticsearch-persistence (~> 0.1.9) + factory_girl (~> 4.7.0) + faker (~> 1.6.6) + i18n (~> 0.7.0) + lazyhash (~> 0.1.1) + minitest (~> 5.10.1) + minitest-capybara (~> 0.8.2)! + minitest-ci (~> 3.1.0) + minitest-hooks (~> 1.4.0) + minitest-reporters (~> 1.1.13) + mongoid (~> 6.0.3) + multi_json (~> 1.12.1) + nokogiri (~> 1.6.8) + oj (~> 2.18.0) + poltergeist (~> 1.11.0) + rails_compatible_cookies_utils (~> 0.1.0) + rainbow (~> 2.1.0) + rake (~> 11.3.0) + rubocop (~> 0.46.0) + typhoeus (~> 1.1.2) + zonebie (~> 0.6.1) + +BUNDLED WITH + 1.13.6 diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..2f560861e --- /dev/null +++ b/Rakefile @@ -0,0 +1,25 @@ +require "bundler/setup" + +# Detect the source root directory. +API_UMBRELLA_SRC_ROOT = File.expand_path("../", __FILE__) +if(!File.exist?(File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella"))) + raise "The calculated root directory does not appear correct: #{API_UMBRELLA_SRC_ROOT}" +end + +# Add our build directories to the $PATH. This ensures test and development +# dependencies (like nodejs/yarn) are on the path. +ENV["PATH"] = [ + "#{API_UMBRELLA_SRC_ROOT}/build/work/stage/opt/api-umbrella/embedded/bin", + "#{API_UMBRELLA_SRC_ROOT}/build/work/stage/opt/api-umbrella/embedded/sbin", + "#{API_UMBRELLA_SRC_ROOT}/build/work/test-env/bin", + "#{API_UMBRELLA_SRC_ROOT}/build/work/test-env/sbin", + "#{API_UMBRELLA_SRC_ROOT}/build/work/dev-env/bin", + "#{API_UMBRELLA_SRC_ROOT}/build/work/dev-env/sbin", + "#{API_UMBRELLA_SRC_ROOT}/build/work/cmake/bin", + ENV["PATH"], +].join(":") + +Dir.glob(File.join(API_UMBRELLA_SRC_ROOT, "scripts/rake/*.rake")).each { |r| import r } + +task(:default).clear +task(:default => [:lint, :test]) diff --git a/Vagrantfile b/Vagrantfile index c17cf4dae..783b5ec27 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -61,6 +61,16 @@ Vagrant.configure("2") do |config| config.nfs.map_gid = Process.gid end + config.vm.synced_folder "src/api-umbrella/admin-ui", "/vagrant-admin-ui", + :type => "rsync", + :rsync__verbose => true, + :rsync__exclude => [ + "tmp", + "node_modules", + "bower_components", + "dist", + ] + # Provider-specific configuration so you can fine-tune various # backing providers for Vagrant. These expose provider-specific options. config.vm.provider :virtualbox do |vb| diff --git a/bin/api-umbrella-geoip-auto-updater b/bin/api-umbrella-geoip-auto-updater index f4aa66aa8..6da42b0cd 100755 --- a/bin/api-umbrella-geoip-auto-updater +++ b/bin/api-umbrella-geoip-auto-updater @@ -40,13 +40,11 @@ while true; do echo "Downloading new file..." unzip_path=$(mktemp -t api-umbrella-geoip-auto-updater.XXXXXXXXXX) download_path="$unzip_path.gz" - curl --silent --show-error --fail --location --output "$download_path" "$download_url" - if [ $? -ne 0 ]; then + if ! curl --silent --show-error --fail --location --output "$download_path" "$download_url"; then echo "Error downloading $download_url" else # Un-gzip the downloaded file. - gunzip -c "$download_path" > "$unzip_path" - if [ $? -ne 0 ]; then + if ! gunzip -c "$download_path" > "$unzip_path"; then echo "Error unzipping $download_path" else unzip_md5=$(openssl md5 "$unzip_path" | awk '{print $2}') @@ -62,8 +60,7 @@ while true; do mv "$unzip_path" "$current_path" echo "Installed new $current_path" - "$API_UMBRELLA_SRC_ROOT"/bin/api-umbrella reload - if [ $? == 0 ]; then + if "$API_UMBRELLA_SRC_ROOT"/bin/api-umbrella reload; then echo "Reloaded api-umbrella" else echo "Reloading api-umbrella failed" diff --git a/build/cmake/core-admin-ui.cmake b/build/cmake/core-admin-ui.cmake new file mode 100644 index 000000000..19cd9107b --- /dev/null +++ b/build/cmake/core-admin-ui.cmake @@ -0,0 +1,85 @@ +file(GLOB_RECURSE admin_ui_files + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/app/*.hbs + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/app/*.html + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/app/*.js + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/app/*.scss + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/config/*.js + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/lib/*.js +) +add_custom_command( + OUTPUT + ${STAMP_DIR}/core-admin-ui-build-dir + ${CORE_BUILD_DIR}/tmp/admin-ui-build/bower.json + ${CORE_BUILD_DIR}/tmp/admin-ui-build/package.json + DEPENDS + ${admin_ui_files} + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/bower.json + ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/package.json + COMMAND mkdir -p ${CORE_BUILD_DIR}/tmp/admin-ui-build + COMMAND rsync -a -v --delete-after "--filter=:- ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/.gitignore" --exclude=/dist-prod --exclude=/dist-dev ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/ ${CORE_BUILD_DIR}/tmp/admin-ui-build/ + COMMAND touch ${STAMP_DIR}/core-admin-ui-build-dir +) + +add_custom_command( + OUTPUT + ${STAMP_DIR}/core-admin-ui-yarn-install + ${CORE_BUILD_DIR}/tmp/admin-ui-build/node_modules + DEPENDS + yarn + ${CORE_BUILD_DIR}/tmp/admin-ui-build/package.json + COMMAND cd ${CORE_BUILD_DIR}/tmp/admin-ui-build && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} yarn install && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} yarn clean + COMMAND touch ${STAMP_DIR}/core-admin-ui-yarn-install +) + +add_custom_command( + OUTPUT + ${STAMP_DIR}/core-admin-ui-bower-install + ${CORE_BUILD_DIR}/tmp/admin-ui-build/bower_components + DEPENDS + ${CORE_BUILD_DIR}/tmp/admin-ui-build/bower.json + ${STAMP_DIR}/core-admin-ui-yarn-install + COMMAND cd ${CORE_BUILD_DIR}/tmp/admin-ui-build && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} ./node_modules/.bin/bower install && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} ./node_modules/.bin/bower prune + COMMAND touch ${STAMP_DIR}/core-admin-ui-bower-install +) + +add_custom_command( + OUTPUT + ${STAMP_DIR}/core-admin-ui-build + ${CORE_BUILD_DIR}/tmp/admin-ui-build/dist-dev + ${CORE_BUILD_DIR}/tmp/admin-ui-build/dist-prod + DEPENDS + ${STAMP_DIR}/core-admin-ui-build-dir + ${STAMP_DIR}/core-admin-ui-yarn-install + ${STAMP_DIR}/core-admin-ui-bower-install + COMMAND cd ${CORE_BUILD_DIR}/tmp/admin-ui-build && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} ./node_modules/.bin/ember build --environment=development --output-path=./dist-dev + COMMAND cd ${CORE_BUILD_DIR}/tmp/admin-ui-build && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} ./node_modules/.bin/ember build --environment=production --output-path=./dist-prod + COMMAND touch ${STAMP_DIR}/core-admin-ui-build +) + +# Normally we perform the yarn & bower installs out-of-source (so the build +# takes place entirely out of source), but if testing/development is enabled +# for this build, then also create a local symlink within the source. This then +# allows for easier interactions with the application. +if(ENABLE_TEST_DEPENDENCIES) + add_custom_command( + OUTPUT ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/node_modules + DEPENDS + ${STAMP_DIR}/core-admin-ui-yarn-install + ${CORE_BUILD_DIR}/tmp/admin-ui-build/node_modules + COMMAND rm -rf ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/node_modules + COMMAND ln -snf ${CORE_BUILD_DIR}/tmp/admin-ui-build/node_modules ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/node_modules + COMMAND touch -h ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/node_modules + ) + add_custom_target(core-admin-ui-local-yarn ALL DEPENDS ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/node_modules) + + add_custom_command( + OUTPUT ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/bower_components + DEPENDS + ${STAMP_DIR}/core-admin-ui-bower-install + ${CORE_BUILD_DIR}/tmp/admin-ui-build/bower_components + COMMAND rm -rf ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/bower_components + COMMAND ln -snf ${CORE_BUILD_DIR}/tmp/admin-ui-build/bower_components ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/bower_components + COMMAND touch -h ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/bower_components + ) + add_custom_target(core-admin-ui-local-bower ALL DEPENDS ${CMAKE_SOURCE_DIR}/src/api-umbrella/admin-ui/bower_components) +endif() diff --git a/build/cmake/core-web-app.cmake b/build/cmake/core-web-app.cmake index 94ff93c0c..1b22564e9 100644 --- a/build/cmake/core-web-app.cmake +++ b/build/cmake/core-web-app.cmake @@ -1,13 +1,33 @@ add_custom_command( - OUTPUT ${STAMP_DIR}/core-web-app-bundle + OUTPUT + ${STAMP_DIR}/core-web-app-bundle + ${WORK_DIR}/src/web-app/.bundle + ${VENDOR_DIR}/bundle DEPENDS bundler ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile.lock COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/web-app/.bundle bundle install --clean --path=${VENDOR_DIR}/bundle + COMMAND touch -c ${WORK_DIR}/src/web-app/.bundle + COMMAND touch -c ${VENDOR_DIR}/bundle COMMAND touch ${STAMP_DIR}/core-web-app-bundle ) +file(GLOB_RECURSE web_asset_files + ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/*.css + ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/*.erb + ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/*.js +) +add_custom_command( + OUTPUT ${STAMP_DIR}/core-web-app-precompile + DEPENDS + ${STAMP_DIR}/core-web-app-bundle + ${web_asset_files} + ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/config/initializers/assets.rb + COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/web-app/.bundle RAILS_TMP_PATH=/tmp/web-app-build RAILS_PUBLIC_PATH=${CORE_BUILD_DIR}/tmp/web-app-build bundle exec rake -f ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Rakefile assets:clobber assets:precompile + COMMAND touch ${STAMP_DIR}/core-web-app-precompile +) + # Normally we perform the bundle out-of-source (so the build takes place # entirely out of source), but if testing/development is enabled for this # build, then also create a local ".bundle/config" item within the source. This @@ -17,22 +37,12 @@ if(ENABLE_TEST_DEPENDENCIES) add_custom_command( OUTPUT ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/.bundle/config DEPENDS - bundler ${STAMP_DIR}/core-web-app-bundle - ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile - ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile.lock - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile bundle install --clean --path=${VENDOR_DIR}/bundle - COMMAND touch ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/.bundle/config + ${WORK_DIR}/src/web-app/.bundle + ${VENDOR_DIR}/bundle + COMMAND rm -rf ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/.bundle + COMMAND ln -snf ${WORK_DIR}/src/web-app/.bundle ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/.bundle + COMMAND touch -c ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/.bundle/config ) add_custom_target(core-web-app-local-bundle ALL DEPENDS ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/.bundle/config) endif() - -add_custom_command( - OUTPUT ${STAMP_DIR}/core-web-app-assets-precompiled - DEPENDS - ${VENDOR_DIR}/bundle - ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/**/* - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/web-app/.bundle RAILS_TMP_PATH=${WORK_DIR}/src/web-app/tmp RAILS_PUBLIC_PATH=${WORK_DIR}/src/web-app/public DEVISE_SECRET_KEY=temp RAILS_SECRET_TOKEN=temp RAILS_GROUPS=assets RAILS_ENV=production bundle exec rake assets:precompile - COMMAND touch ${STAMP_DIR}/core-web-app-assets-precompiled -) diff --git a/build/cmake/core.cmake b/build/cmake/core.cmake index 9f7da4cb7..ebf6fd4dc 100644 --- a/build/cmake/core.cmake +++ b/build/cmake/core.cmake @@ -1,5 +1,9 @@ set(CORE_BUILD_DIR ${WORK_DIR}/src/api-umbrella-core) +include(${CMAKE_SOURCE_DIR}/build/cmake/core-lua-deps.cmake) +include(${CMAKE_SOURCE_DIR}/build/cmake/core-web-app.cmake) +include(${CMAKE_SOURCE_DIR}/build/cmake/core-admin-ui.cmake) + # Copy the vendored libraries into the shared build directory. add_custom_command( OUTPUT ${CORE_BUILD_DIR}/shared/vendor @@ -11,58 +15,37 @@ add_custom_command( COMMAND touch -c ${CORE_BUILD_DIR}/shared/vendor ) -# Copy the precompiled assets into the shared build directory. -add_custom_command( - OUTPUT ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/public/web-assets - DEPENDS ${STAMP_DIR}/core-web-app-assets-precompiled - COMMAND mkdir -p ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/public/web-assets - COMMAND rsync -a --delete-after ${WORK_DIR}/src/web-app/public/web-assets/ ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/public/web-assets/ - COMMAND touch -c ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/public/web-assets -) - -# Create the tmp directories in the shared build directory. -# -# We create these more specific tmp sub directories so the deb/rpm -# after-install script can set the necessary permissions on these sub -# directories to allow for deployments of master on top of a package install. -add_custom_command( - OUTPUT ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/assets - COMMAND mkdir -p ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/assets - COMMAND touch -c ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/assets -) -add_custom_command( - OUTPUT ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/sass - COMMAND mkdir -p ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/sass - COMMAND touch -c ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/sass -) -add_custom_command( - OUTPUT ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/ember-rails - COMMAND mkdir -p ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/ember-rails - COMMAND touch -c ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/ember-rails -) - -# -# Build the shared dir. -# -add_custom_command( - OUTPUT ${STAMP_DIR}/core-build-shared - DEPENDS - ${CORE_BUILD_DIR}/shared/vendor - ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/public/web-assets - ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/assets - ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/cache/sass - ${CORE_BUILD_DIR}/shared/src/api-umbrella/web-app/tmp/ember-rails - COMMAND touch ${STAMP_DIR}/core-build-shared -) - # Copy the code into a build release directory. +file(GLOB_RECURSE core_files + ${CMAKE_SOURCE_DIR}/bin/* + ${CMAKE_SOURCE_DIR}/config/* + ${CMAKE_SOURCE_DIR}/templates/* +) add_custom_command( OUTPUT ${STAMP_DIR}/core-build-release-dir + DEPENDS ${core_files} COMMAND mkdir -p ${CORE_BUILD_DIR}/releases/0 COMMAND rsync -a --delete-after --delete-excluded "--filter=:- ${CMAKE_SOURCE_DIR}/.gitignore" --include=/templates/etc/perp/.boot --exclude=.* --exclude=/templates/etc/test-env* --exclude=/templates/etc/perp/test-env* --exclude=/src/api-umbrella/web-app/spec --exclude=/src/api-umbrella/web-app/app/assets --exclude=/src/api-umbrella/hadoop-analytics --include=/bin/*** --include=/config/*** --include=/LICENSE.txt --include=/templates/*** --include=/src/*** --exclude=* ${CMAKE_SOURCE_DIR}/ ${CORE_BUILD_DIR}/releases/0/ COMMAND touch ${STAMP_DIR}/core-build-release-dir ) +add_custom_command( + OUTPUT + ${STAMP_DIR}/core-build-install-dist + ${CORE_BUILD_DIR}/releases/0/build/dist/web-app-assets + ${CORE_BUILD_DIR}/releases/0/build/dist/admin-ui-dev + ${CORE_BUILD_DIR}/releases/0/build/dist/admin-ui + DEPENDS + ${STAMP_DIR}/core-admin-ui-build + ${STAMP_DIR}/core-web-app-precompile + ${STAMP_DIR}/core-build-release-dir + COMMAND mkdir -p ${CORE_BUILD_DIR}/releases/0/build/dist/web-app-assets + COMMAND rsync -a --delete-after ${CORE_BUILD_DIR}/tmp/web-app-build/web-assets/ ${CORE_BUILD_DIR}/releases/0/build/dist/web-app-assets/web-assets/ + COMMAND rsync -a --delete-after ${CORE_BUILD_DIR}/tmp/admin-ui-build/dist-dev/ ${CORE_BUILD_DIR}/releases/0/build/dist/admin-ui-dev/ + COMMAND rsync -a --delete-after ${CORE_BUILD_DIR}/tmp/admin-ui-build/dist-prod/ ${CORE_BUILD_DIR}/releases/0/build/dist/admin-ui/ + COMMAND touch ${STAMP_DIR}/core-build-install-dist +) + # Create a symlink to the latest release. add_custom_command( OUTPUT ${STAMP_DIR}/core-build-current-symlink @@ -97,28 +80,6 @@ add_custom_command( COMMAND touch -c ${CORE_BUILD_DIR}/releases/0/src/api-umbrella/web-app/.bundle/config ) -# Create a symlink to the shared assets directory within the release. -add_custom_command( - OUTPUT ${STAMP_DIR}/core-build-release-web-assets-symlink - DEPENDS - ${STAMP_DIR}/core-build-release-dir - ${STAMP_DIR}/core-build-shared - WORKING_DIRECTORY ${CORE_BUILD_DIR}/releases/0/src/api-umbrella/web-app/public - COMMAND ln -snf ../../../../../../shared/src/api-umbrella/web-app/public/web-assets ./web-assets - COMMAND touch ${STAMP_DIR}/core-build-release-web-assets-symlink -) - -# Create a symlink to the shared web-app tmp directory within the release. -add_custom_command( - OUTPUT ${STAMP_DIR}/core-build-release-web-tmp-symlink - DEPENDS - ${STAMP_DIR}/core-build-release-dir - ${STAMP_DIR}/core-build-shared - WORKING_DIRECTORY ${CORE_BUILD_DIR}/releases/0/src/api-umbrella/web-app - COMMAND ln -snf ../../../../../shared/src/api-umbrella/web-app/tmp ./tmp - COMMAND touch ${STAMP_DIR}/core-build-release-web-tmp-symlink -) - # # Build the release dir. # @@ -127,8 +88,6 @@ add_custom_command( DEPENDS ${STAMP_DIR}/core-build-release-dir ${STAMP_DIR}/core-build-release-vendor-symlink - ${STAMP_DIR}/core-build-release-web-assets-symlink - ${STAMP_DIR}/core-build-release-web-tmp-symlink ${STAMP_DIR}/core-build-current-symlink ${CORE_BUILD_DIR}/releases/0/src/api-umbrella/web-app/.bundle/config COMMAND touch ${STAMP_DIR}/core-build-release @@ -137,8 +96,9 @@ add_custom_command( # Copy the built shared directory to the stage install path. add_custom_command( OUTPUT ${STAGE_EMBEDDED_DIR}/apps/core - DEPENDS ${STAMP_DIR}/core-build-shared + DEPENDS ${CORE_BUILD_DIR}/shared/vendor DEPENDS ${STAMP_DIR}/core-build-release + DEPENDS ${STAMP_DIR}/core-build-install-dist COMMAND mkdir -p ${STAGE_EMBEDDED_DIR}/apps/core COMMAND rsync -a --delete-after ${CORE_BUILD_DIR}/ ${STAGE_EMBEDDED_DIR}/apps/core/ COMMAND touch -c ${STAGE_EMBEDDED_DIR}/apps/core diff --git a/build/cmake/dev/nodejs.cmake b/build/cmake/dev/nodejs.cmake new file mode 100644 index 000000000..bae76a42b --- /dev/null +++ b/build/cmake/dev/nodejs.cmake @@ -0,0 +1,21 @@ +# NodeJS: For building admin-ui Ember app. +ExternalProject_Add( + nodejs + URL https://nodejs.org/dist/v${NODEJS_VERSION}/node-v${NODEJS_VERSION}-linux-x64.tar.xz + URL_HASH SHA256=${NODEJS_HASH} + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND rsync -a -v / ${DEV_INSTALL_PREFIX}/ +) + +ExternalProject_Add( + yarn + DEPENDS nodejs + URL https://github.com/yarnpkg/yarn/releases/download/v${YARN_VERSION}/yarn-v${YARN_VERSION}.tar.gz + URL_HASH MD5=${YARN_HASH} + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND rsync -a -v --delete / ${DEV_INSTALL_PREFIX}/yarn/ + COMMAND cd ${DEV_INSTALL_PREFIX}/bin && ln -snf ../yarn/bin/yarn ./yarn + COMMAND cd ${DEV_INSTALL_PREFIX}/bin && ln -snf ../yarn/bin/yarn.js ./yarn.js +) diff --git a/build/cmake/install.cmake b/build/cmake/install.cmake index 054e3a08d..ca3d41b6f 100644 --- a/build/cmake/install.cmake +++ b/build/cmake/install.cmake @@ -78,10 +78,6 @@ install( execute_process( COMMAND chmod 1777 \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/var/tmp ) - message(STATUS \"Permissions: \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/embedded/apps/core/shared/src/api-umbrella/web-app/tmp\") - execute_process( - COMMAND chmod 775 \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/embedded/apps/core/shared/src/api-umbrella/web-app/tmp - ) " COMPONENT core ) diff --git a/build/cmake/mora.cmake b/build/cmake/mora.cmake index a8d9ffaa7..e237d4c44 100644 --- a/build/cmake/mora.cmake +++ b/build/cmake/mora.cmake @@ -33,8 +33,8 @@ ExternalProject_Add( CONFIGURE_COMMAND "" BUILD_COMMAND cp ${CMAKE_SOURCE_DIR}/build/mora/glide.yaml /glide.yaml COMMAND cp ${CMAKE_SOURCE_DIR}/build/mora/glide.lock /glide.lock - COMMAND env PATH=${GOLANG_SOURCE_DIR}/bin:${GLIDE_SOURCE_DIR}:${WORK_DIR}/gocode/bin:$ENV{PATH} GOPATH=${WORK_DIR}/gocode GOROOT=${GOLANG_SOURCE_DIR} GO15VENDOREXPERIMENT=1 glide install - COMMAND env PATH=${GOLANG_SOURCE_DIR}/bin:${GLIDE_SOURCE_DIR}:${WORK_DIR}/gocode/bin:$ENV{PATH} GOPATH=${WORK_DIR}/gocode GOROOT=${GOLANG_SOURCE_DIR} GO15VENDOREXPERIMENT=1 go install + COMMAND env PATH=${GOLANG_SOURCE_DIR}/bin:${GLIDE_SOURCE_DIR}:${WORK_DIR}/gocode/bin:$ENV{PATH} GOPATH=${WORK_DIR}/gocode GOROOT=${GOLANG_SOURCE_DIR} glide install + COMMAND env PATH=${GOLANG_SOURCE_DIR}/bin:${GLIDE_SOURCE_DIR}:${WORK_DIR}/gocode/bin:$ENV{PATH} GOPATH=${WORK_DIR}/gocode GOROOT=${GOLANG_SOURCE_DIR} go install INSTALL_COMMAND install -D -m 755 ${WORK_DIR}/gocode/bin/mora ${STAGE_EMBEDDED_DIR}/bin/mora ) ExternalProject_Add_Step( diff --git a/build/cmake/outdated.cmake b/build/cmake/outdated.cmake deleted file mode 100644 index ed84399b3..000000000 --- a/build/cmake/outdated.cmake +++ /dev/null @@ -1,15 +0,0 @@ -add_custom_command( - OUTPUT ${STAMP_DIR}/outdated-bundle - DEPENDS - bundler - ${CMAKE_SOURCE_DIR}/build/scripts/Gemfile - ${CMAKE_SOURCE_DIR}/build/scripts/Gemfile.lock - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/build/scripts/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/outdated/.bundle bundle install --clean --path=${WORK_DIR}/src/outdated/bundle - COMMAND touch ${STAMP_DIR}/outdated-bundle -) - -add_custom_target( - outdated - DEPENDS ${STAMP_DIR}/outdated-bundle - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/build/scripts/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/outdated/.bundle ${CMAKE_SOURCE_DIR}/build/scripts/outdated -) diff --git a/build/cmake/ruby.cmake b/build/cmake/ruby.cmake index b4730272a..e0dae52d1 100644 --- a/build/cmake/ruby.cmake +++ b/build/cmake/ruby.cmake @@ -1,7 +1,7 @@ # Ruby & Bundler: For Rails web-app component ExternalProject_Add( ruby - URL https://cache.ruby-lang.org/pub/ruby/2.2/ruby-${RUBY_VERSION}.tar.bz2 + URL https://cache.ruby-lang.org/pub/ruby/ruby-${RUBY_VERSION}.tar.bz2 URL_HASH SHA256=${RUBY_HASH} CONFIGURE_COMMAND rm -rf && mkdir -p # Clean across version upgrades COMMAND /configure --prefix=${INSTALL_PREFIX_EMBEDDED} --enable-load-relative --disable-install-doc diff --git a/build/cmake/test/bundle.cmake b/build/cmake/test/bundle.cmake new file mode 100644 index 000000000..1869be32b --- /dev/null +++ b/build/cmake/test/bundle.cmake @@ -0,0 +1,33 @@ +add_custom_command( + OUTPUT + ${STAMP_DIR}/test-bundle + ${WORK_DIR}/.bundle + ${WORK_DIR}/bundle + DEPENDS + bundler + ${CMAKE_SOURCE_DIR}/Gemfile + ${CMAKE_SOURCE_DIR}/Gemfile.lock + COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/.bundle bundle install --clean --path=${WORK_DIR}/bundle + COMMAND touch -c ${WORK_DIR}/.bundle + COMMAND touch -c ${WORK_DIR}/bundle + COMMAND touch ${STAMP_DIR}/test-bundle +) + +# Normally we perform the bundle out-of-source (so the build takes place +# entirely out of source), but if testing/development is enabled for this +# build, then also create a local ".bundle/config" item within the source. This +# then allows for gems to be found when interacting with the local source +# version of the app. +if(ENABLE_TEST_DEPENDENCIES) + add_custom_command( + OUTPUT ${CMAKE_SOURCE_DIR}/.bundle/config + DEPENDS + ${STAMP_DIR}/test-bundle + ${WORK_DIR}/.bundle + ${WORK_DIR}/bundle + COMMAND rm -rf ${CMAKE_SOURCE_DIR}/.bundle + COMMAND ln -snf ${WORK_DIR}/.bundle ${CMAKE_SOURCE_DIR}/.bundle + COMMAND touch -c ${CMAKE_SOURCE_DIR}/.bundle/config + ) + add_custom_target(test-local-bundle ALL DEPENDS ${CMAKE_SOURCE_DIR}/.bundle/config) +endif() diff --git a/build/cmake/test/lint.cmake b/build/cmake/test/lint.cmake deleted file mode 100644 index e8726d895..000000000 --- a/build/cmake/test/lint.cmake +++ /dev/null @@ -1,19 +0,0 @@ -add_custom_target( - lint-target - DEPENDS test-lua-deps - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMAND env LUA_PATH=${TEST_VENDOR_LUA_SHARE_DIR}/?.lua$${TEST_VENDOR_LUA_SHARE_DIR}/?/init.lua$$ LUA_CPATH=${TEST_VENDOR_LUA_LIB_DIR}/?.so$$ ${TEST_VENDOR_DIR}/bin/luacheck ${CMAKE_SOURCE_DIR}/src - VERBATIM -) - -add_custom_target( - shell-lint-target - COMMAND ${CMAKE_SOURCE_DIR}/test/scripts/shell-lint -) - -add_custom_target( - lint - COMMAND ${CMAKE_BUILD_TOOL} all - COMMAND ${CMAKE_BUILD_TOOL} lint-target - COMMAND ${CMAKE_BUILD_TOOL} shell-lint-target -) diff --git a/build/cmake/test/mailhog.cmake b/build/cmake/test/mailhog.cmake new file mode 100644 index 000000000..56f0c7bf7 --- /dev/null +++ b/build/cmake/test/mailhog.cmake @@ -0,0 +1,10 @@ +# MailHog: SMTP testing server +ExternalProject_Add( + mailhog + URL https://github.com/mailhog/MailHog/releases/download/v${MAILHOG_VERSION}/MailHog_linux_amd64 + URL_HASH MD5=${MAILHOG_HASH} + DOWNLOAD_NO_EXTRACT 1 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND install -D -m 755 ${TEST_INSTALL_PREFIX}/bin/mailhog +) diff --git a/build/cmake/test/mongo-orchestration.cmake b/build/cmake/test/mongo-orchestration.cmake index 5abbbda50..4e296bb79 100644 --- a/build/cmake/test/mongo-orchestration.cmake +++ b/build/cmake/test/mongo-orchestration.cmake @@ -6,7 +6,6 @@ add_custom_command( add_custom_target(test_virtualenv ALL DEPENDS ${TEST_INSTALL_PREFIX}/bin/pip) add_custom_command( OUTPUT ${TEST_INSTALL_PREFIX}/bin/mongo-orchestration - DEPENDS ${TEST_INSTALL_PREFIX}/bin/pip ${CMAKE_SOURCE_DIR}/test/requirements.txt - COMMAND ${TEST_INSTALL_PREFIX}/bin/pip install -r ${CMAKE_SOURCE_DIR}/test/requirements.txt + COMMAND ${TEST_INSTALL_PREFIX}/bin/pip install --ignore-installed 'mongo-orchestration==${MONGO_ORCHESTRATION_VERSION}' ) add_custom_target(test_pip_install ALL DEPENDS ${TEST_INSTALL_PREFIX}/bin/mongo-orchestration) diff --git a/build/cmake/test/phantomjs.cmake b/build/cmake/test/phantomjs.cmake new file mode 100644 index 000000000..6d3a976e3 --- /dev/null +++ b/build/cmake/test/phantomjs.cmake @@ -0,0 +1,9 @@ +# PhantomJS: Headless WebKit for testing. +ExternalProject_Add( + phantomjs + URL https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-${PHANTOMJS_VERSION}-linux-x86_64.tar.bz2 + URL_HASH MD5=${PHANTOMJS_HASH} + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND install -D -m 755 /bin/phantomjs ${TEST_INSTALL_PREFIX}/bin/phantomjs +) diff --git a/build/cmake/test/shellcheck.cmake b/build/cmake/test/shellcheck.cmake index 77e12107b..5e0b24187 100644 --- a/build/cmake/test/shellcheck.cmake +++ b/build/cmake/test/shellcheck.cmake @@ -1,10 +1,15 @@ ExternalProject_Add( shellcheck - URL https://github.com/caarlos0/shellcheck-docker/releases/download/v${SHELLCHECK_VERSION}/shellcheck - URL_HASH MD5=${SHELLCHECK_HASH} - DOWNLOAD_NAME shellcheck-binz + # Download 64bit linux binary (that works in all distros) for v0.4.5 from + # author: + # https://github.com/koalaman/shellcheck/issues/758#issuecomment-257730652 + # + # Should followup with shellcheck issue about whether they plan to release + # these binaries more formally and on an ongoing basis. + URL https://www.vidarholen.net/%7Evidar/shellcheck.${SHELLCHECK_VERSION}.gz + URL_HASH SHA256=${SHELLCHECK_HASH} DOWNLOAD_NO_EXTRACT 1 CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND install -D -m 755 ${TEST_INSTALL_PREFIX}/bin/shellcheck + BUILD_COMMAND gzip -d -c > /shellcheck + INSTALL_COMMAND install -D -m 755 /shellcheck ${TEST_INSTALL_PREFIX}/bin/shellcheck ) diff --git a/build/cmake/test/test-proxy.cmake b/build/cmake/test/test-proxy.cmake deleted file mode 100644 index bd8e81dc1..000000000 --- a/build/cmake/test/test-proxy.cmake +++ /dev/null @@ -1,20 +0,0 @@ -add_custom_command( - OUTPUT ${CMAKE_SOURCE_DIR}/test/node_modules/.bin/grunt - DEPENDS ${CMAKE_SOURCE_DIR}/test/package.json - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test - COMMAND npm install - COMMAND npm prune -) - -add_custom_target( - test-proxy-target - COMMAND npm test - DEPENDS ${CMAKE_SOURCE_DIR}/test/node_modules/.bin/grunt - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test -) - -add_custom_target( - test-proxy - COMMAND ${CMAKE_BUILD_TOOL} all - COMMAND ${CMAKE_BUILD_TOOL} test-proxy-target -) diff --git a/build/cmake/test/test-web-app.cmake b/build/cmake/test/test-web-app.cmake deleted file mode 100644 index bb31cbcdc..000000000 --- a/build/cmake/test/test-web-app.cmake +++ /dev/null @@ -1,30 +0,0 @@ -add_custom_target( - test-web-app-bundle-audit - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} bundle exec bundle-audit check --update -) - -add_custom_target( - test-web-app-brakeman - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} bundle exec brakeman . --format html --output brakeman.html --exit-on-warn -) - -add_custom_target( - test-web-app-rake - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} INTEGRATION_TEST_SUITE=true bundle exec rake -) - -add_custom_target( - test-web-app-target - COMMAND ${CMAKE_BUILD_TOOL} test-web-app-bundle-audit - COMMAND ${CMAKE_BUILD_TOOL} test-web-app-brakeman - COMMAND ${CMAKE_BUILD_TOOL} test-web-app-rake -) - -add_custom_target( - test-web-app - COMMAND ${CMAKE_BUILD_TOOL} all - COMMAND ${CMAKE_BUILD_TOOL} test-web-app-target -) diff --git a/build/cmake/test/test.cmake b/build/cmake/test/test.cmake index a85b59554..b451b6867 100644 --- a/build/cmake/test/test.cmake +++ b/build/cmake/test/test.cmake @@ -1,3 +1,14 @@ +add_custom_target( + test-deps + DEPENDS ${STAMP_DIR}/test-bundle test-lua-deps +) + +add_custom_target( + test-target + DEPENDS test-deps + COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/.bundle bundle exec rake +) + # CMake policy CMP0037 business to allow target named "test". cmake_policy(PUSH) if(POLICY CMP0037) @@ -6,8 +17,6 @@ endif() add_custom_target( test COMMAND ${CMAKE_BUILD_TOOL} all - COMMAND ${CMAKE_BUILD_TOOL} lint-target - COMMAND ${CMAKE_BUILD_TOOL} test-proxy-target - COMMAND ${CMAKE_BUILD_TOOL} test-web-app-target + COMMAND ${CMAKE_BUILD_TOOL} test-target ) cmake_policy(POP) diff --git a/build/cmake/trafficserver.cmake b/build/cmake/trafficserver.cmake index bec2dc211..75db18b69 100644 --- a/build/cmake/trafficserver.cmake +++ b/build/cmake/trafficserver.cmake @@ -3,7 +3,8 @@ ExternalProject_Add( trafficserver URL http://mirror.olnevhost.net/pub/apache/trafficserver/trafficserver-${TRAFFICSERVER_VERSION}.tar.bz2 URL_HASH MD5=${TRAFFICSERVER_HASH} - CONFIGURE_COMMAND env SPHINXBUILD=false LDFLAGS=-Wl,-rpath,${STAGE_EMBEDDED_DIR}/lib /configure --prefix=${INSTALL_PREFIX_EMBEDDED} --enable-experimental-plugins + CONFIGURE_COMMAND rm -rf && mkdir -p # Clean across version upgrades + COMMAND env SPHINXBUILD=false LDFLAGS=-Wl,-rpath,${STAGE_EMBEDDED_DIR}/lib /configure --prefix=${INSTALL_PREFIX_EMBEDDED} --enable-experimental-plugins INSTALL_COMMAND make install DESTDIR=${STAGE_DIR} # Trim our own distribution by removing some larger files we don't need for # API Umbrella. diff --git a/build/cmake/versions.cmake b/build/cmake/versions.cmake index d6e23c045..b1909a7dc 100644 --- a/build/cmake/versions.cmake +++ b/build/cmake/versions.cmake @@ -1,8 +1,8 @@ # Define the versions of the various dependencies to build. set(API_UMBRELLA_STATIC_SITE_VERSION faa3ee34c8d8edb68b81f13cd88d869207110e29) set(API_UMBRELLA_STATIC_SITE_HASH bd9b1c14d2fa211eeb3df1c632afd1c3) -set(BUNDLER_VERSION 1.12.6) -set(BUNDLER_HASH 2d15046557e079bb4a8d8ad69956a7f9bff8a14a5d75860e18781ce2d79f2deb) +set(BUNDLER_VERSION 1.13.6) +set(BUNDLER_HASH fafd22dfed658ca0603f321bdd168ed709d7c682e61273b55637716459f2d0f7) set(ELASTICSEARCH_VERSION 1.7.5) set(ELASTICSEARCH_HASH 23353fcfdf4897997eb36624afa0af62562f9b3c) set(FLUME_VERSION 1.6.0) @@ -13,8 +13,8 @@ set(GLIDE_VERSION 0.12.3) set(GLIDE_HASH 6bf0971dc56fb8ce0c4ba5d20e9ac0f7) set(GOLANG_VERSION 1.7.3) set(GOLANG_HASH 508028aac0654e993564b6e2014bf2d4a9751e3b286661b0b0040046cf18028e) -set(KYLIN_VERSION 1.5.4) -set(KYLIN_HASH 9f0c7cd5e02cd6c8098ab061a9d712f6) +set(KYLIN_VERSION 1.5.4.1) +set(KYLIN_HASH 23834b03d295726de4b933402b778f1c) set(LIBCIDR_VERSION 1.2.3) set(LIBCIDR_HASH c5efcc7ae114fdaa5583f58dacecd9de) set(LIBESTR_VERSION 0.1.10) @@ -35,8 +35,8 @@ set(LUAROCK_INSPECT_VERSION 3.1.0-1) set(LUAROCK_INSPECT_HASH 50c0f238a459ec3ef9d880faf4613689) set(LUAROCK_LIBCIDR_VERSION 0.1.2-1) set(LUAROCK_LIBCIDR_HASH b6bdc9431cb488de8b58a83117107f7a) -set(LUAROCK_LUACHECK_VERSION 0.16.3-1) -set(LUAROCK_LUACHECK_HASH 5e9302c0fcbdef4c8896f9c109f9ba12) +set(LUAROCK_LUACHECK_VERSION 0.17.0-1) +set(LUAROCK_LUACHECK_HASH 255847964af1f5147fb85f3a6b17a46e) set(LUAROCK_LUAPOSIX_VERSION 33.4.0-1) set(LUAROCK_LUAPOSIX_HASH e413e0b9a680a040d8fb1adfa0dc4c11) set(LUAROCK_LUASOCKET_VERSION 2.0.2-6) @@ -63,14 +63,19 @@ set(LUA_RESTY_SHCACHE_VERSION fb2e275c2cdca08eaa34a7b73375e41ac3eff200) set(LUA_RESTY_SHCACHE_HASH 5d3cbcf8fbad1954cdcb3826afa41afe) set(MAVEN_VERSION 3.3.9) set(MAVEN_HASH 516923b3955b6035ba6b0a5b031fbd8b) -set(MONGODB_VERSION 3.0.14) -set(MONGODB_HASH 331cc3c9efee1e97520eacd67fb80115) -set(MORA_VERSION 4cae0b86a440356cc3b669fb76343ac514c99655) -set(MORA_HASH 6764886ca9b8c5302e93597c4500bfd3) +set(MAILHOG_VERSION 0.2.1) +set(MAILHOG_HASH 6602fd7f69276e7efba310362e958133) +set(MONGO_ORCHESTRATION_VERSION 0.6.7) +set(MONGODB_VERSION 3.2.11) +set(MONGODB_HASH 9916a076bd2e2fa8e8fbad94bb083fae) +set(MORA_VERSION a4543bce9676b367ca2af7fcad0589dc87c3d60d) +set(MORA_HASH 99f1c5c2026d202d253976ba43b94c6f) set(NGX_DYUPS_VERSION d4b3e053dee10e2879882eb4c346ac7d534e2d14) set(NGX_DYUPS_HASH bdf4408599602afa38365a426e126d21) set(NGX_TXID_VERSION f1c197cb9c42e364a87fbb28d5508e486592ca42) set(NGX_TXID_HASH 408ee86eb6e42e27a51514f711c41d6b) +set(NODEJS_VERSION 6.9.1) +set(NODEJS_HASH d4eb161e4715e11bbef816a6c577974271e2bddae9cf008744627676ff00036a) set(OPENRESTY_VERSION 1.11.2.1) set(OPENRESTY_HASH f26d152f40c5263b383a5b7c826a6c7e) set(OPENSSL_VERSION 1.0.2j) @@ -79,21 +84,27 @@ set(PCRE_VERSION 8.39) set(PCRE_HASH e3fca7650a0556a2647821679d81f585) set(PERP_VERSION 2.07) set(PERP_HASH a2acc7425d556d9635a25addcee9edb5) -set(PRESTO_VERSION 0.152.1) -set(PRESTO_HASH 43dae590acc3bbd8d91b460bddf3f0ea) -set(RUBY_VERSION 2.2.6) -set(RUBY_HASH e845ba41ea3525aafaa4094212f1eadc57392732232b67b4394a7e0f046dddf7) +set(PHANTOMJS_VERSION 2.1.1) +set(PHANTOMJS_HASH 1c947d57fce2f21ce0b43fe2ed7cd361) +set(PRESTO_VERSION 0.153) +set(PRESTO_HASH 135e41b0388d629fe5dc520b573eb0b0) +set(RUBY_VERSION 2.3.3) +set(RUBY_HASH 882e6146ed26c6e78c02342835f5d46b86de95f0dc4e16543294bc656594cc5b) # Hold at rsyslog 8.14.0 until ElasticSearch issues are fixed (hopefully 8.20): # https://github.com/rsyslog/rsyslog/issues/748 set(RSYSLOG_VERSION 8.14.0) set(RSYSLOG_HASH 443b5b1d2b84f5cd429d06d230af7fb2352336fa6449cb6484dbd4418a7ae7c2) set(RUNIT_VERSION 2.1.2) set(RUNIT_HASH 6c985fbfe3a34608eb3c53dc719172c4) -set(SHELLCHECK_VERSION 0.4.5) -set(SHELLCHECK_HASH a90ae414cd1ad64d547917a47bf3a365) +# v0.4.5, but using this git commit reference for binary from +# https://github.com/koalaman/shellcheck/issues/758#issuecomment-257730652 +set(SHELLCHECK_VERSION 5efb724a) +set(SHELLCHECK_HASH 733f2f8104d12d0e8b7f5e2ee5a1c0e21d4817535cb1b8a29c9f764e26baf2a7) # Don't move to TrafficServer 6 until we can verify keepalive behavior: # https://issues.apache.org/jira/browse/TS-3959 set(TRAFFICSERVER_VERSION 5.3.2) set(TRAFFICSERVER_HASH c8e5f3e81da643ea79cba0494ed37d45) set(UNBOUND_VERSION 1.5.10) set(UNBOUND_HASH a39b8b4fcca2a2b35a2daa53fe35150cc3f09038dc9acede09c912fc248a9486) +set(YARN_VERSION 0.17.9) +set(YARN_HASH 1d49af2d8a76dc9e3e0058f6d0ad63af) diff --git a/build/mora/glide.lock b/build/mora/glide.lock index 9c154b2b4..bd89a6df9 100644 --- a/build/mora/glide.lock +++ b/build/mora/glide.lock @@ -1,5 +1,5 @@ -hash: 1ff0351176d51da4f684e0e7621f3ee5fce0c74c44d7460be4a8107a4a549ee8 -updated: 2016-03-26T17:43:18.223474817-06:00 +hash: f341223f9b59f72bf4242c21178a6e272612d03abaf460892a1bf246c9967c3f +updated: 2016-12-01T10:25:55.897154602-07:00 imports: - name: github.com/compose/mejson version: afcf51c7c640b1e09c17065d4e5b8de3259d6261 @@ -7,17 +7,13 @@ imports: version: 89af920d613f1e3f771f6460b2629632e7a36ae9 subpackages: - swagger -- name: github.com/emicklei/mora - version: fea22d544a961ef5cd66dde405cf452b1ae33d2c - subpackages: - - api/documents - - api/response - - api/statistics - - session - name: github.com/magiconair/properties version: c265cfa48dda6474e208715ca93e987829f572f8 - name: gopkg.in/mgo.v2 - version: d90005c5262a3463800497ea5a89aed5fe22c886 + version: b6121c6199b7beecde08e6853f256a6475fe8031 subpackages: - bson -devImports: [] + - internal/json + - internal/sasl + - internal/scram +testImports: [] diff --git a/build/mora/glide.yaml b/build/mora/glide.yaml index 7d26e861b..8fdc0087e 100644 --- a/build/mora/glide.yaml +++ b/build/mora/glide.yaml @@ -1,4 +1,4 @@ -package: . +package: github.com/emicklei/mora import: - package: github.com/compose/mejson - package: github.com/emicklei/go-restful @@ -7,15 +7,9 @@ import: version: ~1.1.3 subpackages: - swagger -- package: github.com/emicklei/mora - subpackages: - - api/documents - - api/response - - api/statistics - - session - package: github.com/magiconair/properties version: ~1.7.0 - package: gopkg.in/mgo.v2 - version: r2016.02.04 + version: r2016.08.01 subpackages: - bson diff --git a/build/package/scripts/after-install b/build/package/scripts/after-install index 3e500283b..e46106339 100755 --- a/build/package/scripts/after-install +++ b/build/package/scripts/after-install @@ -35,7 +35,6 @@ if [ "$configure" = "true" ]; then deploy_group=api-umbrella-deploy prefix_dir=/opt/api-umbrella embedded_dir=$prefix_dir/embedded - web_app_shared_dir=$embedded_dir/apps/core/shared/src/api-umbrella/web-app # Create the main user & group. if ! getent group $group > /dev/null; then @@ -80,14 +79,6 @@ if [ "$configure" = "true" ]; then # Set file permissions chown -R $user:$group $prefix_dir/etc $prefix_dir/var chown -R $deploy_user:$deploy_group $embedded_dir/apps - chown -R $user:$deploy_group $web_app_shared_dir/tmp - chown -R $deploy_user:$group $web_app_shared_dir/tmp/ember-rails \ - $web_app_shared_dir/tmp/cache/assets \ - $web_app_shared_dir/tmp/cache/sass - chmod -R 775 $web_app_shared_dir/tmp/ember-rails \ - $web_app_shared_dir/tmp/cache/assets \ - $web_app_shared_dir/tmp/cache/sass - chmod 1777 $web_app_shared_dir/tmp if [ -d $embedded_dir/kylin ]; then chown $user:$group $embedded_dir/kylin/tomcat/temp \ $embedded_dir/kylin/tomcat/webapps \ diff --git a/build/package_dependencies.sh b/build/package_dependencies.sh index 1edd23e88..fd417f84c 100644 --- a/build/package_dependencies.sh +++ b/build/package_dependencies.sh @@ -84,6 +84,12 @@ if [ -f /etc/redhat-release ]; then hadoop_analytics_build_dependencies=( java-1.8.0-openjdk-devel ) + test_build_dependencies=( + # Unbound + bison + expat-devel + flex + ) elif [ -f /etc/debian_version ]; then libffi_version=6 openjdk_version=7 @@ -159,6 +165,12 @@ elif [ -f /etc/debian_version ]; then hadoop_analytics_build_dependencies=( openjdk-$openjdk_version-jdk ) + test_build_dependencies=( + # Unbound + bison + flex + libexpat-dev + ) if [[ "$ID" == "debian" && "$VERSION_ID" == "8" ]] || [[ "$ID" == "ubuntu" && "$VERSION_ID" == "16.04" ]]; then core_build_dependencies+=("libtool-bin") @@ -174,4 +186,5 @@ all_dependencies=( "${hadoop_analytics_package_dependencies[@]}" "${core_build_dependencies[@]}" "${hadoop_analytics_build_dependencies[@]}" + "${test_build_dependencies[@]}" ) diff --git a/build/scripts/Gemfile b/build/scripts/Gemfile deleted file mode 100644 index b7e40cd6b..000000000 --- a/build/scripts/Gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source "https://rubygems.org" - -# Colorized console printing -gem "rainbow", "~> 2.1.0" diff --git a/build/scripts/Gemfile.lock b/build/scripts/Gemfile.lock deleted file mode 100644 index bc60d0448..000000000 --- a/build/scripts/Gemfile.lock +++ /dev/null @@ -1,13 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - rainbow (2.1.0) - -PLATFORMS - ruby - -DEPENDENCIES - rainbow (~> 2.1.0) - -BUNDLED WITH - 1.11.2 diff --git a/build/scripts/distclean b/build/scripts/distclean index f6a18abe1..9accefe14 100755 --- a/build/scripts/distclean +++ b/build/scripts/distclean @@ -28,5 +28,4 @@ rm -rf \ "$source_dir/src/api-umbrella/web-app/spec/reports" \ "$source_dir/src/api-umbrella/web-app/tmp"/* \ "$source_dir/test/config/.overrides.yml" \ - "$source_dir/test/node_modules"/* \ "$source_dir/test/tmp"/* diff --git a/build/scripts/download_cmake b/build/scripts/download_cmake index 8fb434f28..af2bc49ca 100755 --- a/build/scripts/download_cmake +++ b/build/scripts/download_cmake @@ -2,8 +2,8 @@ set -e -u -version=3.6.2 -checksum=5df4b69d9e85093ae78b1070d5cb9f824ce0bdd02528948c3f6a740e240083e5 +version=3.7.0 +checksum=e075f63e6a9104b1c3d11666ae9546bc8812f7e791a49c4ce11effc063141b2a source_dir="$(dirname "$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")")" version_stamp="$source_dir/build/work/cmake/version-$version" diff --git a/build/scripts/outdated b/build/scripts/outdated deleted file mode 100755 index f7c801dec..000000000 --- a/build/scripts/outdated +++ /dev/null @@ -1,333 +0,0 @@ -#!/usr/bin/env ruby - -require "bundler/setup" -require "json" -require "open-uri" -require "rainbow" - -repos = { - "api_umbrella_static_site" => { - :git => "https://github.com/NREL/api-umbrella-static-site.git", - :git_ref => "master", - }, - "bundler" => { - :git => "https://github.com/bundler/bundler.git", - }, - "elasticsearch" => { - :git => "https://github.com/elasticsearch/elasticsearch.git", - :constraint => "~> 1.7.5", - }, - "flume" => { - :git => "https://github.com/apache/flume.git", - }, - "glide" => { - :git => "https://github.com/Masterminds/glide.git", - }, - "golang" => { - :git => "https://go.googlesource.com/go", - }, - "json_c" => { - :git => "https://github.com/json-c/json-c.git", - }, - "kylin" => { - :git => "https://github.com/apache/kylin.git", - }, - "libcidr" => { - :http => "https://www.over-yonder.net/~fullermd/projects/libcidr", - }, - "libestr" => { - :git => "https://github.com/rsyslog/libestr.git", - }, - "libgeoip" => { - :git => "https://github.com/maxmind/geoip-api-c.git", - }, - "librdkafka" => { - :git => "https://github.com/edenhill/librdkafka.git", - }, - "luarocks" => { - :git => "https://github.com/keplerproject/luarocks.git", - }, - "luarock_argparse" => { - :luarock => "argparse", - }, - "luarock_cmsgpack" => { - :luarock => "lua-cmsgpack", - }, - "luarock_iconv" => { - :luarock => "lua-iconv", - }, - "luarock_inspect" => { - :luarock => "inspect", - }, - "luarock_libcidr" => { - :luarock => "libcidr-ffi", - }, - "luarock_luacheck" => { - :luarock => "luacheck", - }, - "luarock_luaposix" => { - :luarock => "luaposix", - }, - "luarock_luasocket" => { - :luarock => "luasocket", - }, - "luarock_luatz" => { - :luarock => "luatz", - }, - "luarock_lustache" => { - :luarock => "lustache", - }, - "luarock_lyaml" => { - :luarock => "lyaml", - }, - "luarock_penlight" => { - :luarock => "penlight", - }, - "luarock_resty_auto_ssl" => { - :luarock => "lua-resty-auto-ssl", - }, - "luarock_resty_http" => { - :luarock => "lua-resty-http", - }, - "luarock_resty_uuid" => { - :luarock => "lua-resty-uuid", - }, - "lua_resty_dns_cache" => { - :git => "https://github.com/hamishforbes/lua-resty-dns-cache.git", - :git_ref => "master", - }, - "lua_resty_logger_socket" => { - :git => "https://github.com/cloudflare/lua-resty-logger-socket.git", - :git_ref => "master", - }, - "lua_resty_shcache" => { - :git => "https://github.com/cloudflare/lua-resty-shcache.git", - :git_ref => "master", - }, - "maven" => { - :git => "https://github.com/apache/maven.git", - }, - "mongodb" => { - :git => "https://github.com/mongodb/mongo.git", - :constraint => "~> 3.0.10", - }, - "mora" => { - :git => "https://github.com/emicklei/mora.git", - :git_ref => "master", - }, - "ngx_dyups" => { - :git => "https://github.com/yzprofile/ngx_http_dyups_module.git", - :git_ref => "master", - }, - "ngx_txid" => { - :git => "https://github.com/streadway/ngx_txid.git", - :git_ref => "master", - }, - "openresty" => { - :git => "https://github.com/openresty/openresty.git", - }, - "openssl" => { - :git => "https://github.com/openssl/openssl.git", - :string_version => true, - }, - "pcre" => { - :http => "http://ftp.csx.cam.ac.uk/pub/software/programming/pcre/", - }, - "perp" => { - :http => "http://b0llix.net/perp/site.cgi?page=download", - }, - "presto" => { - :git => "https://github.com/facebook/presto.git", - }, - "ruby" => { - :git => "https://github.com/ruby/ruby.git", - :constraint => "~> 2.2.4", - }, - "rsyslog" => { - :git => "https://github.com/rsyslog/rsyslog.git", - :constraint => "< 8.15.0", - }, - "runit" => { - :http => "http://smarden.org/runit/install.html", - }, - "shellcheck" => { - :git => "https://github.com/koalaman/shellcheck.git", - }, - "trafficserver" => { - :git => "https://github.com/apache/trafficserver.git", - :constraint => "~> 5.3.2", - }, - "unbound" => { - :http => "https://www.unbound.net/download.html", - }, -} - -luarocks_manifest = JSON.load(open("https://luarocks.org/manifest.json")) - -def luarock_version_to_semver(version) - version.gsub(/-(\d+)$/, '.0.0.\1') -end - -def semver_to_luarock_version(version) - version.gsub(/\.0\.0\.(\d+)$/, '-\1') -end - -def tag_to_semver(name, tag) - tag.downcase! - - # Remove prefixes containing the project name. - tag.gsub!(/^#{name}[\-_]/i, "") - tag.gsub!(/^#{name.gsub("_", "-")}[\-_]/i, "") - - # Remove trailing "^{}" at end of git tags. - tag.chomp!("^{}") - - # Remove "release-" prefixes. - tag.gsub!(/^release-/, "") - - # Remove "v" or "r" prefixes before the version number. - tag.gsub!(/^[vr](\d)/, '\1') - - # Project-specific normalizations. - case(name) - when "golang" - tag.gsub!(/^go/, "") - when "json_c" - tag.gsub!(/-\d{8}$/, "") - when "openssl", "ruby" - tag.gsub!(/_/, ".") - end - - tag -end - -versions = {} -versions_content = File.read(File.expand_path("../../cmake/versions.cmake", __FILE__)) -versions_content.each_line do |line| - current_version_matches = line.match(/set\((.+?)_VERSION (.+?)\)/) - if(!current_version_matches) - next - end - - name = current_version_matches[1].downcase - options = repos[name] || {} - current_version_string = current_version_matches[2] - - begin - if(options[:luarock]) - current_version = Gem::Version.new(luarock_version_to_semver(current_version_string)) - else - current_version = Gem::Version.new(current_version_string) - end - rescue ArgumentError - current_version = current_version_string.dup - end - versions[name] = { - :current_version => current_version, - } - - constraint = Gem::Dependency.new(name, options[:constraint]) - - tags = [] - unparsable_tags = [] - - if(options[:git] && options[:git_ref]) - current_commit = current_version_string - if(current_commit !~ /^[0-9a-f]{5,40}$/) - current_commit = `git ls-remote #{options[:git]} #{current_version_string}`.split(/\s/).first - if(current_commit.to_s.empty?) - puts "#{name}: Could not parse version #{current_version_string}" - end - end - - latest_commit = `git ls-remote #{options[:git]} #{options[:git_ref]}`.split(/\s/).first - if(latest_commit.to_s.empty?) - puts "#{name}: Could not parse latest commit: git ls-remote #{options[:git]} #{options[:git_ref]}" - end - - versions[name][:current_version] = current_commit[0,7] - versions[name][:latest_version] = latest_commit[0,7] - versions[name][:wanted_version] = latest_commit[0,7] - elsif(options[:git]) - tags = `git ls-remote --tags #{options[:git]}`.lines - tags.map! { |tag| tag_to_semver(name, tag.match(%r{refs/tags/(.+)$})[1]) } - elsif(options[:svn]) - tags = `svn ls #{options[:svn]}`.lines - tags.map! { |tag| tag_to_semver(name, tag) } - elsif(options[:luarock]) - tags = luarocks_manifest["repository"][options[:luarock]].keys - tags.map! { |tag| luarock_version_to_semver(tag) } - elsif(options[:http]) - content = open(options[:http]).read - tags = content.scan(/#{name}-[\d\.]+.tar/) - tags.map! { |f| tag_to_semver(name, File.basename(f, ".tar")) } - end - - case(name) - when "openssl" - tags.select! { |tag| tag =~ /^\d+\.\d+\.\d+[a-z]?$/ } - end - - tags.compact! - tags.uniq! - tags.each do |tag| - if(options[:string_version]) - available_version = tag - if(!versions[name][:latest_version] || available_version > versions[name][:latest_version]) - versions[name][:latest_version] = available_version - versions[name][:wanted_version] = available_version - end - else - begin - available_version = Gem::Version.new(tag) - next if(available_version.prerelease?) - - if(!versions[name][:latest_version] || available_version > versions[name][:latest_version]) - versions[name][:latest_version] = available_version - end - - if(constraint.match?(name, available_version)) - if(!versions[name][:wanted_version] || available_version > versions[name][:wanted_version]) - versions[name][:wanted_version] = available_version - end - end - rescue ArgumentError => e - unparsable_tags << tag - end - end - end - - if(unparsable_tags.any?) - puts "#{name}: Could not parse version tag #{unparsable_tags.join(", ")}" - end -end - -puts "\n\n" - -print Rainbow("Package".ljust(32)).underline -print Rainbow("Current".rjust(16)).underline -print Rainbow("Wanted".rjust(16)).underline -print Rainbow("Latest".rjust(16)).underline -puts "" - -versions.each do |name, info| - name_column = name.ljust(32) - if(info[:wanted_version].to_s != info[:current_version].to_s) - print Rainbow(name_column).red - elsif(info[:current_version].to_s != info[:latest_version].to_s) - print Rainbow(name_column).yellow - else - print name_column - end - - if(repos[name] && repos[name][:luarock]) - info[:current_version] = semver_to_luarock_version(info[:current_version].to_s) - info[:wanted_version] = semver_to_luarock_version(info[:wanted_version].to_s) - info[:latest_version] = semver_to_luarock_version(info[:latest_version].to_s) - end - - print info[:current_version].to_s.rjust(16) - print Rainbow(info[:wanted_version].to_s.rjust(16)).green - print Rainbow(info[:latest_version].to_s.rjust(16)).magenta - puts "" -end diff --git a/circle.yml b/circle.yml index b716fc4d0..e545bebe9 100644 --- a/circle.yml +++ b/circle.yml @@ -5,7 +5,7 @@ general: - src/api-umbrella/web-app/log - src/api-umbrella/web-app/brakeman.html # Keep screenshots of capybara failures for easier debugging. - - src/api-umbrella/web-app/tmp/capybara + - test/tmp/capybara machine: pre: # Enable IPv6 on CircleCI for running IPv6 integration tests. @@ -13,16 +13,15 @@ machine: # Disable RVM so the various environment variables (things like GEM_HOME) # don't conflict with our embedded ruby installation. - rvm reset - node: - version: 0.10.45 dependencies: cache_directories: - build/work - CMakeCache.txt - CMakeFiles - cmake_install.cmake - - test/node_modules pre: + # Install any system package dependencies. + - sudo ./build/scripts/install_build_dependencies # Set the local file timestamps based on when they were last modified in # git. This helps prevent unnecessary rebuilds in the CI environment, since # make is dependent on file timestamps and every CI test run operates on a @@ -37,11 +36,16 @@ dependencies: override: # Build all the API Umbrella software dependencies. - ./configure --enable-test-dependencies - - make + - make all test-deps # Remove the download archives, since we don't need to cache these in # CircleCI, and doing so also leads to multiple versions being kept around # whenever we bump our dependency versions. - make clean-download-archives +database: + override: + # Don't perform any database tasks that CircleCI infers, since they're not + # needed. + - /bin/true compile: override: # Don't perform any compile tasks that CircleCI infers. We'll run our @@ -49,14 +53,7 @@ compile: - /bin/true test: override: - # Run the across parallel CI nodes. - - test/circle_parallel: - parallel: true - - # Copy the test reports into place. - - mkdir -p $CIRCLE_TEST_REPORTS/rspec $CIRCLE_TEST_REPORTS/mocha: - parallel: true - - if [ -e src/api-umbrella/web-app/spec/reports/web-app.xml ]; then cp src/api-umbrella/web-app/spec/reports/web-app.xml $CIRCLE_TEST_REPORTS/rspec/; fi: - parallel: true - - if [ -e test/tmp/xunit$CIRCLE_NODE_INDEX.xml ]; then cp test/tmp/xunit$CIRCLE_NODE_INDEX.xml $CIRCLE_TEST_REPORTS/mocha/; fi: + - ./test/scripts/circle-ci: parallel: true + files: + - test/**/test_*.rb diff --git a/config/default.yml b/config/default.yml index 6188d668b..a2b0b2614 100644 --- a/config/default.yml +++ b/config/default.yml @@ -99,9 +99,6 @@ web: client_secret: ldap: options: - myusa: - client_id: - client_secret: static_site: host: 127.0.0.1 port: 14013 @@ -109,6 +106,7 @@ router: api_backends: host: 127.0.0.1 port: 14011 + keepalive_connections: 10 trusted_proxies: [] global_rate_limits: ip_rate: @@ -130,14 +128,7 @@ dns_resolver: retries: 3 mongodb: url: "mongodb://127.0.0.1:14001/api_umbrella" - options: - server: - auto_reconnect: true - socketOptions: - keepAlive: 500 - replset: - socketOptions: - keepAlive: 500 + read_preference: primaryPreferred embedded_server_config: processManagement: fork: false @@ -354,3 +345,6 @@ ban: status_code: 403 delay: 0 message: "Please contact us for assistance." +ember_server: + port: 14050 + live_reload_port: 14051 diff --git a/config/test.yml b/config/test.yml new file mode 100644 index 000000000..8a8eca7d8 --- /dev/null +++ b/config/test.yml @@ -0,0 +1,115 @@ +app_env: test +user: +group: +http_port: 9080 +https_port: 9081 +root_dir: /tmp/api-umbrella-test +analytics: + timezone: America/Denver + log_request_url_query_params_separately: true +static_site: + port: 13013 +nginx: + # We default the number of workers to the number of CPU cores, but keep this + # hard-coded for test purposes so we have a more stable baseline and ensure + # our tests always run with multiple workers. + workers: 2 + access_log_options: + proxy_connect_timeout: 10 + proxy_read_timeout: 10 + proxy_send_timeout: 10 + dyups: + port: 13005 +trafficserver: + port: 13009 + autoconf_port: 13007 + management_port: 13008 + embedded_server_config: + records: + # In the test environment disable fuzzy revalidation since it makes + # things difficult to test against. For production, it seems like a + # decent idea to keep the feature enabled, but in the test environment, + # it means that 0.5% of requests might not behave as expected (since they + # will be re-fetched ahead of their actual TTL). + # See: https://docs.trafficserver.apache.org/en/latest/admin/http-proxy-caching.en.html?highlight=fuzz#fuzzy-revalidation + - CONFIG proxy.config.http.cache.fuzz.time INT 0 +api_server: + port: 13010 +web: + port: 13012 + admin: + initial_superusers: + - initial.admin@example.com + auth_strategies: + enabled: + - facebook + - github + - google + - ldap + - max.gov + - persona + facebook: + client_id: test_fake + client_secret: test_fake + github: + client_id: test_fake + client_secret: test_fake + google: + client_id: test_fake + client_secret: test_fake + ldap: + options: + host: 127.0.0.1 + port: 389 + method: plain + base: dc=example,dc=com + uid: sAMAccountName + mailer: + smtp_settings: + address: 127.0.0.1 + port: 13102 + contact_form_email: default-test-contact-email@example.com +router: + api_backends: + port: 13011 + trusted_proxies: + - 192.168.12.0/23 + - 10.10.10.10 +rsyslog: + port: 13014 +mongodb: + url: "mongodb://127.0.0.1:13001/api_umbrella_test" + embedded_server_config: + net: + port: 13001 + storage: + journal: + enabled: false +mora: + port: 13004 +elasticsearch: + hosts: + - "http://127.0.0.1:13002" + embedded_server_config: + http: + port: 13002 + transport: + tcp: + port: 13003 + discovery: + zen: + ping: + multicast: + enabled: false + index: + number_of_shards: 1 + number_of_replicas: 0 +unbound: + port: 13100 + control_port: 13101 +mailhog: + smtp_bind_addr: "127.0.0.1:13102" + api_bind_addr: "127.0.0.1:13103" + ui_bind_addr: "127.0.0.1:13103" +apiSettings: + require_https: optional diff --git a/deploy/config/deploy.rb b/deploy/config/deploy.rb index 70d100e79..1fb943347 100644 --- a/deploy/config/deploy.rb +++ b/deploy/config/deploy.rb @@ -32,7 +32,6 @@ set :linked_dirs, fetch(:linked_dirs, []).push( "build/work", "src/api-umbrella/web-app/public/web-assets", - "src/api-umbrella/web-app/tmp", "vendor" ) @@ -89,18 +88,6 @@ before :updated, :lua_deps before :reverted, :lua_deps - # The ember-rails gem's handling of temp files isn't ideal when multiple users - # might touch the files. So for now, just make these temp files globally - # writable. See: - # https://github.com/emberjs/ember-rails/issues/315#issuecomment-47703370 - # https://github.com/emberjs/ember-rails/pull/357 - task :ember_permissions do - on roles(:app) do - execute "mkdir -p #{release_path}/src/api-umbrella/web-app/tmp/ember-rails && chmod -R 777 #{release_path}/src/api-umbrella/web-app/tmp/ember-rails" - end - end - after :updated, :ember_permissions - task :compile_assets do on roles(:app) do within("#{release_path}/src/api-umbrella/web-app") do diff --git a/scripts/rake/lint.rake b/scripts/rake/lint.rake new file mode 100644 index 000000000..a0d84bfd6 --- /dev/null +++ b/scripts/rake/lint.rake @@ -0,0 +1,90 @@ +namespace :lint do + desc "Lint JavaScript files using eslint" + task :js do + require "childprocess" + require "rainbow" + + js_files = `cd #{API_UMBRELLA_SRC_ROOT}/src/api-umbrella/admin-ui && git ls-files | grep "\.js$"`.split("\n") + js_files -= [ + ".eslintrc.js", + "lib/inject-live-reload/index.js", + "tests/.eslintrc.js", + ] + + print "Checking: #{js_files.join(" ")}... " + process = ChildProcess.build("node_modules/eslint/bin/eslint.js", "--cache", *js_files) + process.cwd = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/admin-ui") + process.io.inherit! + process.start + process.wait + exit(process.exit_code) if(process.crashed?) + puts Rainbow("OK").green.bright + end + + desc "Lint Lua files using luacheck" + task :lua do + require "childprocess" + + lua_files = `git ls-files #{API_UMBRELLA_SRC_ROOT} | grep "\.lua$"`.split("\n") + process = ChildProcess.build("build/work/test-env/vendor/bin/luacheck", *lua_files) + process.cwd = API_UMBRELLA_SRC_ROOT + process.environment["LUA_PATH"] = "build/work/test-env/vendor/share/lua/5.1/?.lua;build/work/test-env/vendor/share/lua/5.1/?/init.lua;;" + process.environment["LUA_CPATH"] = "build/work/test-env/vendor/lib/lua/5.1/?.so;;" + process.io.inherit! + process.start + process.wait + exit(process.exit_code) if(process.crashed?) + end + + require "rubocop/rake_task" + RuboCop::RakeTask.new(:ruby) do |t| + t.patterns = [ + File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/web-app/**/*.rb"), + File.join(API_UMBRELLA_SRC_ROOT, "test/**/*.rb"), + File.join(API_UMBRELLA_SRC_ROOT, "Rakefile"), + File.join(API_UMBRELLA_SRC_ROOT, "script/rake/*.rb"), + File.join(API_UMBRELLA_SRC_ROOT, "script/rake/*.rake"), + ] + t.options = [ + "--display-cop-names", + "--extra-details", + ] + end + + desc "Lint shell files using shellcheck" + task :shell do + require "childprocess" + require "rainbow" + + # Ignore certain vendored files for linting. + ignore_files = [ + "configure", + "templates/etc/perp/kylin/rc.run.mustache", + ] + + ["sh", "bash"].each do |shell| + shell_files = `git grep -l "^#\!/bin/#{shell}" #{API_UMBRELLA_SRC_ROOT}`.split("\n") + shell_files += `git grep -l "^#\!/usr/bin/env #{shell}" #{API_UMBRELLA_SRC_ROOT}`.split("\n") + shell_files -= ignore_files + + if(shell_files.any?) + print "Checking (#{shell}): #{shell_files.join(" ")}... " + process = ChildProcess.build("build/work/test-env/bin/shellcheck", "-s", shell, *shell_files) + process.cwd = API_UMBRELLA_SRC_ROOT + process.io.inherit! + process.start + process.wait + exit(process.exit_code) if(process.crashed?) + puts Rainbow("OK").green.bright + end + end + end +end + +desc "Lint all source code for errors and style" +task :lint do + Rake::Task["lint:lua"].invoke + Rake::Task["lint:ruby"].invoke + Rake::Task["lint:js"].invoke + Rake::Task["lint:shell"].invoke +end diff --git a/scripts/rake/outdated.rake b/scripts/rake/outdated.rake new file mode 100644 index 000000000..46eae9b20 --- /dev/null +++ b/scripts/rake/outdated.rake @@ -0,0 +1,79 @@ +namespace :outdated do + namespace "admin-ui" do + desc "List outdated admin-ui Bower dependencies" + task :bower do + require "childprocess" + process = ChildProcess.build("./node_modules/.bin/bower", "list") + process.cwd = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/admin-ui") + process.io.inherit! + process.start + process.wait + end + + desc "List outdated admin-ui NPM dependencies" + task :npm do + require "childprocess" + process = ChildProcess.build("yarn", "outdated") + process.cwd = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/admin-ui") + process.io.inherit! + process.start + process.wait + end + end + + namespace "test" do + desc "List outdated test gem dependencies" + task :gems do + require "childprocess" + Bundler.with_original_env do + process = ChildProcess.build("bundle", "outdated") + process.cwd = API_UMBRELLA_SRC_ROOT + process.io.inherit! + process.start + process.wait + end + end + end + + namespace "web-app" do + desc "List outdated web-app gem dependencies" + task :gems do + require "childprocess" + Bundler.with_original_env do + process = ChildProcess.build("bundle", "outdated") + process.cwd = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/web-app") + process.io.inherit! + process.start + process.wait + end + end + end + + desc "List outdated package dependencies" + task :packages do + require_relative "./outdated_packages" + OutdatedPackages.new + end +end + +desc "List outdated dependencies" +task :outdated do + puts "==== ADMIN-UI: NPM ====" + Rake::Task["outdated:admin-ui:npm"].invoke + puts "\n\n" + + puts "==== ADMIN-UI: BOWER ====" + Rake::Task["outdated:admin-ui:bower"].invoke + puts "\n\n" + + puts "==== WEB-APP: GEMS ====" + Rake::Task["outdated:web-app:gems"].invoke + puts "\n\n" + + puts "==== TEST: GEMS ====" + Rake::Task["outdated:test:gems"].invoke + puts "\n\n" + + puts "==== PACKAGES ====" + Rake::Task["outdated:packages"].invoke +end diff --git a/scripts/rake/outdated_packages.rb b/scripts/rake/outdated_packages.rb new file mode 100644 index 000000000..63e84d4ac --- /dev/null +++ b/scripts/rake/outdated_packages.rb @@ -0,0 +1,354 @@ +require "json" +require "open-uri" +require "rainbow" + +class OutdatedPackages + REPOS = { + "api_umbrella_static_site" => { + :git => "https://github.com/NREL/api-umbrella-static-site.git", + :git_ref => "master", + }, + "bundler" => { + :git => "https://github.com/bundler/bundler.git", + }, + "elasticsearch" => { + :git => "https://github.com/elasticsearch/elasticsearch.git", + :constraint => "~> 1.7.5", + }, + "flume" => { + :git => "https://github.com/apache/flume.git", + }, + "glide" => { + :git => "https://github.com/Masterminds/glide.git", + }, + "golang" => { + :git => "https://go.googlesource.com/go", + }, + "json_c" => { + :git => "https://github.com/json-c/json-c.git", + }, + "kylin" => { + :git => "https://github.com/apache/kylin.git", + }, + "libcidr" => { + :http => "https://www.over-yonder.net/~fullermd/projects/libcidr", + }, + "libestr" => { + :git => "https://github.com/rsyslog/libestr.git", + }, + "libgeoip" => { + :git => "https://github.com/maxmind/geoip-api-c.git", + }, + "librdkafka" => { + :git => "https://github.com/edenhill/librdkafka.git", + }, + "luarocks" => { + :git => "https://github.com/keplerproject/luarocks.git", + }, + "luarock_argparse" => { + :luarock => "argparse", + }, + "luarock_cmsgpack" => { + :luarock => "lua-cmsgpack", + }, + "luarock_iconv" => { + :luarock => "lua-iconv", + }, + "luarock_inspect" => { + :luarock => "inspect", + }, + "luarock_libcidr" => { + :luarock => "libcidr-ffi", + }, + "luarock_luacheck" => { + :luarock => "luacheck", + }, + "luarock_luaposix" => { + :luarock => "luaposix", + }, + "luarock_luasocket" => { + :luarock => "luasocket", + }, + "luarock_luatz" => { + :luarock => "luatz", + }, + "luarock_lustache" => { + :luarock => "lustache", + }, + "luarock_lyaml" => { + :luarock => "lyaml", + }, + "luarock_penlight" => { + :luarock => "penlight", + }, + "luarock_resty_auto_ssl" => { + :luarock => "lua-resty-auto-ssl", + }, + "luarock_resty_http" => { + :luarock => "lua-resty-http", + }, + "luarock_resty_uuid" => { + :luarock => "lua-resty-uuid", + }, + "lua_resty_dns_cache" => { + :git => "https://github.com/hamishforbes/lua-resty-dns-cache.git", + :git_ref => "master", + }, + "lua_resty_logger_socket" => { + :git => "https://github.com/cloudflare/lua-resty-logger-socket.git", + :git_ref => "master", + }, + "lua_resty_shcache" => { + :git => "https://github.com/cloudflare/lua-resty-shcache.git", + :git_ref => "master", + }, + "maven" => { + :git => "https://github.com/apache/maven.git", + }, + "mailhog" => { + :git => "https://github.com/mailhog/MailHog.git", + }, + "mongo_orchestration" => { + :git => "https://github.com/10gen/mongo-orchestration.git", + }, + "mongodb" => { + :git => "https://github.com/mongodb/mongo.git", + :constraint => "~> 3.2.9", + }, + "mora" => { + :git => "https://github.com/emicklei/mora.git", + :git_ref => "master", + }, + "ngx_dyups" => { + :git => "https://github.com/yzprofile/ngx_http_dyups_module.git", + :git_ref => "master", + }, + "ngx_txid" => { + :git => "https://github.com/streadway/ngx_txid.git", + :git_ref => "master", + }, + "nodejs" => { + :git => "https://github.com/nodejs/node.git", + :constraint => "~> 6.9.1", + }, + "openresty" => { + :git => "https://github.com/openresty/openresty.git", + }, + "openssl" => { + :git => "https://github.com/openssl/openssl.git", + :string_version => true, + }, + "pcre" => { + :http => "http://ftp.csx.cam.ac.uk/pub/software/programming/pcre/", + }, + "perp" => { + :http => "http://b0llix.net/perp/site.cgi?page=download", + }, + "phantomjs" => { + :git => "https://github.com/ariya/phantomjs.git", + }, + "presto" => { + :git => "https://github.com/facebook/presto.git", + }, + "ruby" => { + :git => "https://github.com/ruby/ruby.git", + }, + "rsyslog" => { + :git => "https://github.com/rsyslog/rsyslog.git", + :constraint => "< 8.15.0", + }, + "runit" => { + :http => "http://smarden.org/runit/install.html", + }, + "shellcheck" => { + :git => "https://github.com/koalaman/shellcheck.git", + }, + "trafficserver" => { + :git => "https://github.com/apache/trafficserver.git", + :constraint => "~> 5.3.2", + }, + "unbound" => { + :http => "https://www.unbound.net/download.html", + }, + "yarn" => { + :git => "https://github.com/yarnpkg/yarn.git", + }, + } + + def luarocks_manifest + @luarocks_manifest ||= JSON.load(open("https://luarocks.org/manifest.json")) + end + + def luarock_version_to_semver(version) + version.gsub(/-(\d+)$/, '.0.0.\1') + end + + def semver_to_luarock_version(version) + version.gsub(/\.0\.0\.(\d+)$/, '-\1') + end + + def tag_to_semver(name, tag) + tag.downcase! + + # Remove prefixes containing the project name. + tag.gsub!(/^#{name}[\-_]/i, "") + tag.gsub!(/^#{name.gsub("_", "-")}[\-_]/i, "") + + # Remove trailing "^{}" at end of git tags. + tag.chomp!("^{}") + + # Remove "release-" prefixes. + tag.gsub!(/^release-/, "") + + # Remove "v" or "r" prefixes before the version number. + tag.gsub!(/^[vr](\d)/, '\1') + + # Project-specific normalizations. + case(name) + when "golang" + tag.gsub!(/^go/, "") + when "json_c" + tag.gsub!(/-\d{8}$/, "") + when "openssl", "ruby" + tag.gsub!(/_/, ".") + end + + tag + end + + def initialize + versions = {} + versions_content = File.read(File.join(API_UMBRELLA_SRC_ROOT, "build/cmake/versions.cmake")) + versions_content.each_line do |line| + current_version_matches = line.match(/set\((.+?)_VERSION (.+?)\)/) + if(!current_version_matches) + next + end + + name = current_version_matches[1].downcase + options = REPOS[name] || {} + current_version_string = current_version_matches[2] + + begin + if(options[:luarock]) + current_version = Gem::Version.new(luarock_version_to_semver(current_version_string)) + else + current_version = Gem::Version.new(current_version_string) + end + rescue ArgumentError + current_version = current_version_string.dup + end + versions[name] = { + :current_version => current_version, + } + + constraint = Gem::Dependency.new(name, options[:constraint]) + + tags = [] + unparsable_tags = [] + + if(options[:git] && options[:git_ref]) + current_commit = current_version_string + if(current_commit !~ /^[0-9a-f]{5,40}$/) + current_commit = `git ls-remote #{options[:git]} #{current_version_string}`.split(/\s/).first + if(current_commit.to_s.empty?) + puts "#{name}: Could not parse version #{current_version_string}" + end + end + + latest_commit = `git ls-remote #{options[:git]} #{options[:git_ref]}`.split(/\s/).first + if(latest_commit.to_s.empty?) + puts "#{name}: Could not parse latest commit: git ls-remote #{options[:git]} #{options[:git_ref]}" + end + + versions[name][:current_version] = current_commit[0,7] + versions[name][:latest_version] = latest_commit[0,7] + versions[name][:wanted_version] = latest_commit[0,7] + elsif(options[:git]) + tags = `git ls-remote --tags #{options[:git]}`.lines + tags.map! { |tag| tag_to_semver(name, tag.match(%r{refs/tags/(.+)$})[1]) } + elsif(options[:svn]) + tags = `svn ls #{options[:svn]}`.lines + tags.map! { |tag| tag_to_semver(name, tag) } + elsif(options[:luarock]) + tags = luarocks_manifest["repository"][options[:luarock]].keys + tags.map! { |tag| luarock_version_to_semver(tag) } + elsif(options[:http]) + content = open(options[:http]).read + tags = content.scan(/#{name}-[\d\.]+.tar/) + tags.map! { |f| tag_to_semver(name, File.basename(f, ".tar")) } + end + + case(name) + when "openssl" + tags.select! { |tag| tag =~ /^1\.0\.\d+[a-z]?$/ } + when "mailhog" + tags.reject! { |tag| tag =~ /^0\.0\d$/ } + end + + tags.compact! + tags.uniq! + tags.each do |tag| + if(options[:string_version]) + available_version = tag + if(!versions[name][:latest_version] || available_version > versions[name][:latest_version]) + versions[name][:latest_version] = available_version + versions[name][:wanted_version] = available_version + end + else + begin + available_version = Gem::Version.new(tag) + next if(available_version.prerelease?) + + if(!versions[name][:latest_version] || available_version > versions[name][:latest_version]) + versions[name][:latest_version] = available_version + end + + if(constraint.match?(name, available_version)) + if(!versions[name][:wanted_version] || available_version > versions[name][:wanted_version]) + versions[name][:wanted_version] = available_version + end + end + rescue ArgumentError => e + unparsable_tags << tag + end + end + end + + if(unparsable_tags.any?) + puts "#{name}: Could not parse version tag #{unparsable_tags.join(", ")}" + end + end + + puts "\n\n" + + print Rainbow("Package".ljust(32)).underline + print Rainbow("Current".rjust(16)).underline + print Rainbow("Wanted".rjust(16)).underline + print Rainbow("Latest".rjust(16)).underline + puts "" + + versions.each do |name, info| + name_column = name.ljust(32) + if(info[:wanted_version].to_s != info[:current_version].to_s) + print Rainbow(name_column).red + elsif(info[:current_version].to_s != info[:latest_version].to_s) + print Rainbow(name_column).yellow + else + print name_column + end + + if(REPOS[name] && REPOS[name][:luarock]) + info[:current_version] = semver_to_luarock_version(info[:current_version].to_s) + info[:wanted_version] = semver_to_luarock_version(info[:wanted_version].to_s) + info[:latest_version] = semver_to_luarock_version(info[:latest_version].to_s) + end + + print info[:current_version].to_s.rjust(16) + print Rainbow(info[:wanted_version].to_s.rjust(16)).green + print Rainbow(info[:latest_version].to_s.rjust(16)).magenta + puts "" + end + end +end + diff --git a/scripts/rake/test.rake b/scripts/rake/test.rake new file mode 100644 index 000000000..d98520c11 --- /dev/null +++ b/scripts/rake/test.rake @@ -0,0 +1,11 @@ +require "rake/testtask" +Rake::TestTask.new do |t| + # If the TESTS environment variable is set, accept that as a space-delimited + # list of test files to run. + if(ENV["TESTS"]) + t.test_files = FileList[ENV["TESTS"].split(" ")] + else + t.pattern = File.join(API_UMBRELLA_SRC_ROOT, "test/**/test_*.rb") + end + t.warning = false +end diff --git a/src/api-umbrella/admin-ui/.bowerrc b/src/api-umbrella/admin-ui/.bowerrc new file mode 100644 index 000000000..959e1696e --- /dev/null +++ b/src/api-umbrella/admin-ui/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "bower_components", + "analytics": false +} diff --git a/src/api-umbrella/admin-ui/.editorconfig b/src/api-umbrella/admin-ui/.editorconfig new file mode 100644 index 000000000..47c543840 --- /dev/null +++ b/src/api-umbrella/admin-ui/.editorconfig @@ -0,0 +1,34 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.js] +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false +indent_style = space +indent_size = 2 + +[*.css] +indent_style = space +indent_size = 2 + +[*.html] +indent_style = space +indent_size = 2 + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/src/api-umbrella/admin-ui/.ember-cli b/src/api-umbrella/admin-ui/.ember-cli new file mode 100644 index 000000000..59bb55fe9 --- /dev/null +++ b/src/api-umbrella/admin-ui/.ember-cli @@ -0,0 +1,9 @@ +{ + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": true +} diff --git a/src/api-umbrella/admin-ui/.eslintrc.js b/src/api-umbrella/admin-ui/.eslintrc.js new file mode 100644 index 000000000..3454bc141 --- /dev/null +++ b/src/api-umbrella/admin-ui/.eslintrc.js @@ -0,0 +1,42 @@ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 6, + sourceType: 'module' + }, + extends: 'eslint:recommended', + env: { + browser: true + }, + rules: { + 'comma-dangle': ['error', 'always-multiline'], + 'no-var': 'error', + 'object-shorthand': ['error', 'methods'], + 'no-duplicate-imports': 'error', + 'func-call-spacing': ['error', 'never'], + 'keyword-spacing': ['error', { 'before': true, 'after': true, 'overrides': { + 'if': { 'after': false }, + 'for': { 'after': false }, + 'while': { 'after': false }, + 'catch': { 'after': false }, + 'switch': { 'after': false }, + }}], + 'no-trailing-spaces': 'error', + }, + globals: { + '$': true, + 'CommonValidations': true, + 'I18n': true, + 'JsDiff': true, + 'PNotify': true, + '_': true, + 'ace': true, + 'bootbox': true, + 'google': true, + 'inflection': true, + 'jstz': true, + 'marked': true, + 'moment': true, + 'numeral': true, + }, +}; diff --git a/src/api-umbrella/admin-ui/.gitignore b/src/api-umbrella/admin-ui/.gitignore new file mode 100644 index 000000000..4fcd4bf3c --- /dev/null +++ b/src/api-umbrella/admin-ui/.gitignore @@ -0,0 +1,18 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp + +# dependencies +/node_modules +/bower_components + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log +testem.log +/.eslintcache diff --git a/src/api-umbrella/admin-ui/.watchmanconfig b/src/api-umbrella/admin-ui/.watchmanconfig new file mode 100644 index 000000000..e7834e3e4 --- /dev/null +++ b/src/api-umbrella/admin-ui/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/src/api-umbrella/admin-ui/README.md b/src/api-umbrella/admin-ui/README.md new file mode 100644 index 000000000..9ddfe3a57 --- /dev/null +++ b/src/api-umbrella/admin-ui/README.md @@ -0,0 +1,53 @@ +# Api-umbrella-admin + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +* [Git](http://git-scm.com/) +* [Node.js](http://nodejs.org/) (with NPM) +* [Bower](http://bower.io/) +* [Ember CLI](http://ember-cli.com/) +* [PhantomJS](http://phantomjs.org/) + +## Installation + +* `git clone ` this repository +* change into the new directory +* `npm install` +* `bower install` + +## Running / Development + +* `ember server` +* Visit your app at [http://localhost:4200](http://localhost:4200). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +* `ember test` +* `ember test --server` + +### Building + +* `ember build` (development) +* `ember build --environment production` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +* [ember.js](http://emberjs.com/) +* [ember-cli](http://ember-cli.com/) +* Development Browser Extensions + * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) + diff --git a/src/api-umbrella/admin-ui/app/adapters/application.js b/src/api-umbrella/admin-ui/app/adapters/application.js new file mode 100644 index 000000000..a8db7ff64 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/adapters/application.js @@ -0,0 +1,75 @@ +import RESTAdapter from 'ember-data/adapters/rest'; + +export default RESTAdapter.extend({ + // Build the URL using the customizable "urlRoot" attribute that can be set + // on the model class. + buildURL(modelName, id, snapshot) { + if(snapshot && snapshot.type && snapshot.type.urlRoot) { + let url = snapshot.type.urlRoot; + if(id) { + url += '/' + encodeURIComponent(id); + } + + return url; + } else { + return this._super(...arguments); + } + }, + + // Ember data requires that errors from the API be returned as an array. This + // normalizes some of our different error responses, so they're always an + // array. + handleResponse(status, headers, payload) { + if(!this.isSuccess(status, headers, payload)) { + this.normalizePayloadErrors(payload, 'errors'); + this.normalizePayloadErrors(payload, 'error'); + } + + return this._super(...arguments); + }, + + normalizePayloadErrors(payload, key) { + if(payload && payload[key]) { + let rawErrors = payload[key]; + let normalizedErrors = []; + + if(_.isArray(rawErrors)) { + // If an array is returned by the API, no need to process further. + normalizedErrors = rawErrors; + } else if(_.isPlainObject(rawErrors)) { + // Turn an object of error messages into an array of error objects. + for(let field in rawErrors) { + // The value might be an array of error messages. + let messages = _.flatten([rawErrors[field]]); + messages.forEach(function(message) { + normalizedErrors.push({ + field: field, + message: message, + }); + }); + } + } else if(_.isString(rawErrors)) { + // Turn a single string error into an array. + normalizedErrors = [{ + message: rawErrors, + }]; + } else { + // If we have some other type of error response, add an "Unexpected + // error" message. + normalizedErrors = [{ + message: 'Unexpected error', + }]; + } + + if(key === 'errors') { + payload.errors = normalizedErrors; + } else { + // When normalizing another key, like "error", append it to any + // existing items on the expected "errors" attribute. + let existingErrors = payload.errors || []; + payload.errors = existingErrors.concat(normalizedErrors); + delete payload[key]; + } + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/app.js b/src/api-umbrella/admin-ui/app/app.js new file mode 100644 index 000000000..ad66c9314 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Resolver from './resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from './config/environment'; + +let App; + +Ember.MODEL_FACTORY_INJECTIONS = true; + +App = Ember.Application.extend({ + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + Resolver, +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/src/api-umbrella/admin-ui/app/authenticators/devise-server-side.js b/src/api-umbrella/admin-ui/app/authenticators/devise-server-side.js new file mode 100644 index 000000000..8a3c0ca8a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/authenticators/devise-server-side.js @@ -0,0 +1,49 @@ +import Ember from 'ember'; +import Base from 'ember-simple-auth/authenticators/base'; + +export default Base.extend({ + restore() { + // Perform a full validation against the server-side endpoint to verify the + // user's authentication on load. We use this, instead of validating the + // data stored client side, since the user's server-side session may have + // expired, even if the local client data thinks it's authenticated. + return this.authenticate(); + }, + + authenticate() { + return new Ember.RSVP.Promise((resolve, reject) => { + $.ajax({ + url: '/admin/auth', + }).done((data) => { + if(this._validate(data)) { + Ember.run(null, resolve, data); + } else { + Ember.run(null, reject, 'unauthenticated'); + } + }).fail((xhr) => { + Ember.Logger.error('Unexpected error: ' + xhr.status + ' ' + xhr.statusText + ' (' + xhr.readyState + '): ' + xhr.responseText); + bootbox.alert('An unexpected server error occurred during authentication'); + Ember.run(null, reject, 'unexpected_error'); + }); + }); + }, + + invalidate() { + return new Ember.RSVP.Promise((resolve, reject) => { + $.ajax({ + url: '/admin/logout', + method: 'DELETE', + }).done(() => { + Ember.run(null, resolve); + }).fail((xhr) => { + Ember.Logger.error('Unexpected error: ' + xhr.status + ' ' + xhr.statusText + ' (' + xhr.readyState + '): ' + xhr.responseText); + bootbox.alert('An unexpected server error occurred during logout'); + Ember.run(null, reject, 'unexpected_error'); + }); + }); + }, + + _validate(data) { + return (data && data.authenticated === true); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/authorizers/devise-server-side.js b/src/api-umbrella/admin-ui/app/authorizers/devise-server-side.js new file mode 100644 index 000000000..8ba5fa2e0 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/authorizers/devise-server-side.js @@ -0,0 +1,7 @@ +import Base from 'ember-simple-auth/authorizers/base'; + +export default Base.extend({ + authorize(data, callback) { + callback(data.api_key, data.csrf_token); + }, +}); diff --git a/src/api-umbrella/web-app/app/assets/generated_sprites/.gitkeep b/src/api-umbrella/admin-ui/app/components/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/generated_sprites/.gitkeep rename to src/api-umbrella/admin-ui/app/components/.gitkeep diff --git a/src/api-umbrella/admin-ui/app/components/admin-groups/index-table.js b/src/api-umbrella/admin-ui/app/components/admin-groups/index-table.js new file mode 100644 index 000000000..aece29cd1 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/admin-groups/index-table.js @@ -0,0 +1,48 @@ +import Ember from 'ember'; +import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; + +export default Ember.Component.extend({ + didInsertElement() { + this.$().find('table').DataTable({ + serverSide: true, + ajax: '/api-umbrella/v1/admin_groups.json', + pageLength: 50, + order: [[0, 'asc']], + columns: [ + { + data: 'name', + title: 'Name', + defaultContent: '-', + render: _.bind(function(name, type, data) { + if(type === 'display' && name && name !== '-') { + let link = '#/admin_groups/' + data.id + '/edit'; + return '' + _.escape(name) + ''; + } + + return name; + }, this), + }, + { + data: 'api_scope_display_names', + title: 'API Scopes', + orderable: false, + render: DataTablesHelpers.renderListEscaped, + }, + { + data: 'permission_display_names', + title: 'Access', + defaultContent: '-', + orderable: false, + render: DataTablesHelpers.renderListEscaped, + }, + { + data: 'admin_usernames', + title: 'Admins', + defaultContent: '-', + orderable: false, + render: DataTablesHelpers.renderListEscaped, + }, + ], + }); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/admin-groups/record-form.js b/src/api-umbrella/admin-ui/app/components/admin-groups/record-form.js new file mode 100644 index 000000000..90c6a2b24 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/admin-groups/record-form.js @@ -0,0 +1,31 @@ +import Ember from 'ember'; +import Save from 'api-umbrella-admin-ui/mixins/save'; + +export default Ember.Component.extend(Save, { + store: Ember.inject.service(), + + apiScopeOptions: Ember.computed(function() { + return this.get('store').findAll('api-scope'); + }), + + permissionOptions: Ember.computed(function() { + return this.get('store').findAll('admin-permission'); + }), + + actions: { + submit() { + this.saveRecord({ + transitionToRoute: 'admin_groups', + message: 'Successfully saved the admin group "' + _.escape(this.get('model.name')) + '"', + }); + }, + + delete() { + this.destroyRecord({ + prompt: 'Are you sure you want to delete the admin group "' + _.escape(this.get('model.name')) + '"?', + transitionToRoute: 'admin_groups', + message: 'Successfully deleted the admin group "' + _.escape(this.get('model.name')) + '"', + }); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/admins/index-table.js b/src/api-umbrella/admin-ui/app/components/admins/index-table.js new file mode 100644 index 000000000..1b3057d83 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/admins/index-table.js @@ -0,0 +1,84 @@ +import Ember from 'ember'; +import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; + +export default Ember.Component.extend({ + session: Ember.inject.service('session'), + + didInsertElement() { + let dataTable = this.$().find('table').DataTable({ + serverSide: true, + ajax: '/api-umbrella/v1/admins.json', + pageLength: 50, + order: [[0, 'asc']], + columns: [ + { + data: 'username', + name: 'Username', + title: 'Username', + defaultContent: '-', + render: _.bind(function(email, type, data) { + if(type === 'display' && email && email !== '-') { + let link = '#/admins/' + data.id + '/edit'; + return '' + _.escape(email) + ''; + } + + return email; + }, this), + }, + { + data: 'email', + name: 'E-mail', + title: 'E-mail', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'name', + name: 'Name', + title: 'Name', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'group_names', + name: 'Groups', + title: 'Groups', + orderable: false, + render: DataTablesHelpers.renderListEscaped, + }, + { + data: 'last_sign_in_at', + type: 'date', + name: 'Last Signed In', + title: 'Last Signed In', + defaultContent: '-', + render: DataTablesHelpers.renderTime, + }, + { + data: 'created_at', + type: 'date', + name: 'Created', + title: 'Created', + defaultContent: '-', + render: DataTablesHelpers.renderTime, + }, + ], + }); + + dataTable.on('draw.dt', function() { + let params = dataTable.ajax.params(); + delete params.start; + delete params.length; + this.set('queryParams', params); + }.bind(this)); + }, + + downloadUrl: Ember.computed('queryParams', function() { + let params = this.get('queryParams'); + if(params) { + params = $.param(params); + } + + return '/api-umbrella/v1/admins.csv?api_key=' + this.get('session.data.authenticated.api_key') + '&' + params; + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/admins/record-form.js b/src/api-umbrella/admin-ui/app/components/admins/record-form.js new file mode 100644 index 000000000..1749b43d6 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/admins/record-form.js @@ -0,0 +1,32 @@ +import Ember from 'ember'; +import Save from 'api-umbrella-admin-ui/mixins/save'; + +export default Ember.Component.extend(Save, { + session: Ember.inject.service(), + store: Ember.inject.service(), + + groupOptions: Ember.computed(function() { + return this.get('store').findAll('admin-group'); + }), + + currentAdmin: Ember.computed(function() { + return this.get('session.data.authenticated.admin'); + }), + + actions: { + submit() { + this.saveRecord({ + transitionToRoute: 'admins', + message: 'Successfully saved the admin "' + _.escape(this.get('model.username')) + '"', + }); + }, + + delete() { + this.destroyRecord({ + prompt: 'Are you sure you want to delete the admin "' + _.escape(this.get('model.name')) + '"?', + transitionToRoute: 'admins', + message: 'Successfully deleted the admin "' + _.escape(this.get('model.name')) + '"', + }); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/api-scopes/index-table.js b/src/api-umbrella/admin-ui/app/components/api-scopes/index-table.js new file mode 100644 index 000000000..4b6899a85 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/api-scopes/index-table.js @@ -0,0 +1,40 @@ +import Ember from 'ember'; +import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; + +export default Ember.Component.extend({ + didInsertElement() { + this.$().find('table').DataTable({ + serverSide: true, + ajax: '/api-umbrella/v1/api_scopes.json', + pageLength: 50, + order: [[0, 'asc']], + columns: [ + { + data: 'name', + title: 'Name', + defaultContent: '-', + render: _.bind(function(name, type, data) { + if(type === 'display' && name && name !== '-') { + let link = '#/api_scopes/' + data.id + '/edit'; + return '' + _.escape(name) + ''; + } + + return name; + }, this), + }, + { + data: 'host', + title: 'Host', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'path_prefix', + title: 'Path Prefix', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + ], + }); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/api-scopes/record-form.js b/src/api-umbrella/admin-ui/app/components/api-scopes/record-form.js new file mode 100644 index 000000000..0d0900c1d --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/api-scopes/record-form.js @@ -0,0 +1,21 @@ +import Ember from 'ember'; +import Save from 'api-umbrella-admin-ui/mixins/save'; + +export default Ember.Component.extend(Save, { + actions: { + submit() { + this.saveRecord({ + transitionToRoute: 'api_scopes', + message: 'Successfully saved the API scope "' + _.escape(this.get('model.name')) + '"', + }); + }, + + delete() { + this.destroyRecord({ + prompt: 'Are you sure you want to delete the API scope "' + _.escape(this.get('model.name')) + '"?', + transitionToRoute: 'api_scopes', + message: 'Successfully deleted the API scope "' + _.escape(this.get('model.name')) + '"', + }); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/api-users/index-table.js b/src/api-umbrella/admin-ui/app/components/api-users/index-table.js new file mode 100644 index 000000000..45c95623c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/api-users/index-table.js @@ -0,0 +1,66 @@ +import Ember from 'ember'; +import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; + +export default Ember.Component.extend({ + didInsertElement() { + this.$().find('table').DataTable({ + serverSide: true, + ajax: '/api-umbrella/v1/users.json', + pageLength: 50, + order: [[4, 'desc']], + columns: [ + { + data: 'email', + title: 'E-mail', + defaultContent: '-', + render: _.bind(function(email, type, data) { + if(type === 'display' && email && email !== '-') { + let link = '#/api_users/' + data.id + '/edit'; + return '' + _.escape(email) + ''; + } + + return email; + }, this), + }, + { + data: 'first_name', + title: 'First Name', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'last_name', + title: 'Last Name', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'use_description', + title: 'Purpose', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'created_at', + type: 'date', + title: 'Created', + defaultContent: '-', + render: DataTablesHelpers.renderTime, + }, + { + data: 'registration_source', + title: 'Registration Source', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'api_key_preview', + title: 'API Key', + defaultContent: '-', + orderable: false, + render: DataTablesHelpers.renderEscaped, + }, + ], + }); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/api-users/record-form.js b/src/api-umbrella/admin-ui/app/components/api-users/record-form.js new file mode 100644 index 000000000..0b2d8235a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/api-users/record-form.js @@ -0,0 +1,51 @@ +import Ember from 'ember'; +import Save from 'api-umbrella-admin-ui/mixins/save'; + +export default Ember.Component.extend(Save, { + store: Ember.inject.service(), + + throttleByIpOptions: [ + { id: false, name: 'Rate limit by API key' }, + { id: true, name: 'Rate limit by IP address' }, + ], + + enabledOptions: [ + { id: true, name: 'Enabled' }, + { id: false, name: 'Disabled' }, + ], + + roleOptions: Ember.computed(function() { + return this.get('store').findAll('api-user-role'); + }), + + actions: { + apiKeyRevealToggle() { + let $key = this.$().find('.api-key'); + let $toggle = this.$().find('.api-key-reveal-toggle'); + + if($key.data('revealed') === 'true') { + $key.text($key.data('api-key-preview')); + $key.data('revealed', 'false'); + $toggle.text(I18n.t('admin.reveal_action')); + } else { + $key.text($key.data('api-key')); + $key.data('revealed', 'true'); + $toggle.text(I18n.t('admin.hide_action')); + } + }, + + submit() { + this.saveRecord({ + transitionToRoute: 'api_users', + message(model) { + let message = 'Successfully saved the user "' + _.escape(model.get('email')) + '"'; + if(model.get('apiKey')) { + message += '
API Key: ' + _.escape(model.get('apiKey')) + ''; + } + + return message; + }, + }); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/index-table.js b/src/api-umbrella/admin-ui/app/components/apis/index-table.js new file mode 100644 index 000000000..c3332702f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/index-table.js @@ -0,0 +1,148 @@ +import Ember from 'ember'; +import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; + +export default Ember.Component.extend({ + busy: Ember.inject.service('busy'), + reorderActive: false, + + didInsertElement() { + this.set('table', this.$().find('table').DataTable({ + serverSide: true, + ajax: '/api-umbrella/v1/apis.json', + pageLength: 50, + rowCallback(row, data) { + $(row).data('id', data.id); + }, + order: [[0, 'asc']], + columns: [ + { + data: 'name', + title: 'Name', + defaultContent: '-', + render: _.bind(function(name, type, data) { + if(type === 'display' && name && name !== '-') { + let link = '#/apis/' + data.id + '/edit'; + return '' + _.escape(name) + ''; + } + + return name; + }, this), + }, + { + data: 'frontend_host', + title: 'Host', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'frontend_prefixes', + title: 'Prefixes', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'sort_order', + title: 'Matching Order', + defaultContent: '-', + width: 130, + render: DataTablesHelpers.renderEscaped, + }, + { + data: null, + className: 'reorder-handle', + orderable: false, + render() { + return ''; + }, + }, + ], + })); + + this.get('table') + .on('search', _.bind(function(event, settings) { + // Disable reordering if the user tries to filter the table by anything + // (otherwise, our reordering logic won't work, since it relies on the + // neighboring rows). + if(this.get('reorderActive')) { + if(settings.oPreviousSearch && settings.oPreviousSearch.sSearch) { + this.set('reorderActive', false); + } + } + }, this)) + .on('order', _.bind(function(event, settings) { + // Disable reordering if the user tries to sort the table by anything + // other than the sort order (otherwise, our reordering logic won't + // work, since it relies on the neighboring rows). + if(this.get('reorderActive')) { + if(settings.aaSorting && !_.isEqual(settings.aaSorting, [[3, 'asc']])) { + this.set('reorderActive', false); + } + } + }, this)); + + this.$().find('tbody').sortable({ + handle: '.reorder-handle', + placeholder: 'reorder-placeholder', + helper(event, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + }, + stop: _.bind(function(event, ui) { + let row = $(ui.item); + let previousRow = row.prev('tbody tr'); + let moveAfterId = null; + if(previousRow.length > 0) { + moveAfterId = $(previousRow[0]).data('id'); + } + + this.saveReorder(row.data('id'), moveAfterId); + }, this), + }); + }, + + handleReorderChange: function() { + if(this.get('reorderActive')) { + this.$().find('table').addClass('reorder-active'); + this.get('table') + .order([[3, 'asc']]) + .search('') + .draw(); + } else { + this.$().find('table').removeClass('reorder-active'); + } + + let $container = this.$(); + if($container) { + let $buttonText = this.$().find('.reorder-button-text'); + if(this.get('reorderActive')) { + $buttonText.data('originalText', $buttonText.text()); + $buttonText.text('Done'); + } else { + $buttonText.text($buttonText.data('originalText')); + } + } + }.observes('reorderActive'), + + saveReorder(id, moveAfterId) { + this.get('busy').show(); + $.ajax({ + url: '/api-umbrella/v1/apis/' + id + '/move_after.json', + method: 'PUT', + data: { move_after_id: moveAfterId }, + }).done(() => { + this.get('table').draw(); + }).fail((xhr) => { + Ember.Logger.error('Unexpected error: ' + xhr.status + ' ' + xhr.statusText + ' (' + xhr.readyState + '): ' + xhr.responseText); + bootbox.alert('An unexpected error occurred. Please try again.'); + this.get('table').draw(); + }); + }, + + actions: { + toggleReorderApis() { + this.set('reorderActive', !this.get('reorderActive')); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/record-form.js b/src/api-umbrella/admin-ui/app/components/apis/record-form.js new file mode 100644 index 000000000..e9b56f01a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/record-form.js @@ -0,0 +1,83 @@ +import Ember from 'ember'; +import Save from 'api-umbrella-admin-ui/mixins/save'; + +export default Ember.Component.extend(Save, { + backendProtocolOptions: [ + { id: 'http', name: 'http' }, + { id: 'https', name: 'https' }, + ], + + balanceAlgorithmOptions: [ + { id: 'least_conn', name: 'Least Connections' }, + { id: 'round_robin', name: 'Round Robin' }, + { id: 'ip_hash', name: 'Source IP Hash' }, + ], + + actions: { + submit() { + this.saveRecord({ + transitionToRoute: 'apis', + message: 'Successfully saved the "' + _.escape(this.get('model.name')) + '" API backend
Note: Your changes are not yet live. Publish Changes to send your updates live.', + }); + }, + + delete() { + this.destroyRecord({ + prompt: 'Are you sure you want to delete the API backend "' + _.escape(this.get('model.name')) + '"?', + transitionToRoute: 'apis', + message: 'Successfully deleted the "' + _.escape(this.get('model.name')) + '" API backend
Note: Your changes are not yet live. Publish Changes to send your updates live.', + }); + }, + + addUrlMatch() { + this.get('controllers.apis_url_match_form').add(this.get('model'), 'urlMatches'); + this.send('openModal', 'apis/url_match_form'); + }, + + editUrlMatch(urlMatch) { + this.get('controllers.apis_url_match_form').edit(this.get('model'), 'urlMatches', urlMatch); + this.send('openModal', 'apis/url_match_form'); + }, + + deleteUrlMatch(urlMatch) { + this.deleteChildRecord('urlMatches', urlMatch, 'Are you sure you want to remove this URL prefix?'); + }, + + addSubSettings() { + this.get('controllers.apis_sub_settings_form').add(this.get('model'), 'subSettings'); + this.send('openModal', 'apis/sub_settings_form'); + }, + + editSubSettings(subSettings) { + this.get('controllers.apis_sub_settings_form').edit(this.get('model'), 'subSettings', subSettings); + this.send('openModal', 'apis/sub_settings_form'); + }, + + deleteSubSettings(subSettings) { + this.deleteChildRecord('subSettings', subSettings, 'Are you sure you want to remove this URL setting?'); + }, + + addRewrite() { + this.get('controllers.apis_rewrite_form').add(this.get('model'), 'rewrites'); + this.send('openModal', 'apis/rewrite_form'); + }, + + editRewrite(rewrite) { + this.get('controllers.apis_rewrite_form').edit(this.get('model'), 'rewrites', rewrite); + this.send('openModal', 'apis/rewrite_form'); + }, + + deleteRewrite(rewrite) { + this.deleteChildRecord('rewrites', rewrite, 'Are you sure you want to remove this rewrite?'); + }, + }, + + deleteChildRecord(collectionName, record, message) { + let collection = this.get('model').get(collectionName); + bootbox.confirm(message, function(result) { + if(result) { + collection.removeObject(record); + } + }); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/rewrite-form.js b/src/api-umbrella/admin-ui/app/components/apis/rewrite-form.js new file mode 100644 index 000000000..e674648d7 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/rewrite-form.js @@ -0,0 +1,49 @@ +import Ember from 'ember'; +import BufferedProxy from 'ember-buffered-proxy/proxy'; + +export default Ember.Component.extend({ + openModal: false, + matcherTypeOptions: [ + { id: 'route', name: 'Route Pattern' }, + { id: 'regex', name: 'Regular Expression' }, + ], + httpMethodOptions: [ + { id: 'any', name: 'Any' }, + { id: 'GET', name: 'GET' }, + { id: 'POST', name: 'POST' }, + { id: 'PUT', name: 'PUT' }, + { id: 'DELETE', name: 'DELETE' }, + { id: 'HEAD', name: 'HEAD' }, + { id: 'TRACE', name: 'TRACE' }, + { id: 'OPTIONS', name: 'OPTIONS' }, + { id: 'CONNECT', name: 'CONNECT' }, + { id: 'PATCH', name: 'PATCH' }, + ], + + modalTitle: Ember.computed('model', function() { + if(this.get('model.isNew')) { + return 'Add Matching URL Prefix'; + } else { + return 'Edit Matching URL Prefix'; + } + }), + + bufferedModel: Ember.computed('model', function() { + return BufferedProxy.create({ content: this.get('model') }); + }), + + actions: { + submit() { + this.get('bufferedModel').applyChanges(); + if(this.get('model.isNew')) { + this.get('collection').pushObject(this.get('model')); + } + + this.set('openModal', false); + }, + + closed() { + this.get('bufferedModel').discardChanges(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/rewrite-table.js b/src/api-umbrella/admin-ui/app/components/apis/rewrite-table.js new file mode 100644 index 000000000..809013e1c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/rewrite-table.js @@ -0,0 +1,31 @@ +import Ember from 'ember'; +import Sortable from 'api-umbrella-admin-ui/mixins/sortable'; + +export default Ember.Component.extend(Sortable, { + store: Ember.inject.service(), + openModal: false, + + sortableCollection: Ember.computed('model', function() { + return this.get('model.rewrites'); + }), + + actions: { + add() { + this.set('rewriteModel', this.get('store').createRecord('api/rewrite')); + this.set('openModal', true); + }, + + edit(rewrite) { + this.set('rewriteModel', rewrite); + this.set('openModal', true); + }, + + remove(rewrite) { + bootbox.confirm('Are you sure you want to remove this rewrite?', function(response) { + if(response) { + this.get('model.rewrites').removeObject(rewrite); + } + }.bind(this)); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/server-form.js b/src/api-umbrella/admin-ui/app/components/apis/server-form.js new file mode 100644 index 000000000..4a7518fe8 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/server-form.js @@ -0,0 +1,57 @@ +import Ember from 'ember'; +import BufferedProxy from 'ember-buffered-proxy/proxy'; +import Server from 'api-umbrella-admin-ui/models/api/server'; + +export default Ember.Component.extend({ + openModal: false, + + modalTitle: Ember.computed('model', function() { + if(this.get('model.isNew')) { + return 'Add Server'; + } else { + return 'Edit Server'; + } + }), + + bufferedModel: Ember.computed('model', function() { + let owner = Ember.getOwner(this).ownerInjection(); + return BufferedProxy.extend(Server.validationClass).create(owner, { content: this.get('model') }); + }), + + actions: { + open() { + // For new servers, intelligently pick the default port based on the + // backend protocol selected. + if(this.get('bufferedModel') && !this.get('bufferedModel.port')) { + if(this.get('apiBackendProtocol') === 'https') { + this.set('bufferedModel.port', 443); + } else { + this.set('bufferedModel.port', 80); + } + } + }, + + submit() { + this.get('bufferedModel').applyChanges(); + if(this.get('model.isNew')) { + this.get('collection').pushObject(this.get('model')); + } + + // After the first server is added, fill out a default value for the + // "Backend Host" field based on the server's host (because in most + // non-load balancing situations they will match). + if(!this.get('apiBackendHost')) { + let server = this.get('collection.firstObject'); + if(server && server.get('host')) { + this.set('apiBackendHost', server.get('host')); + } + } + + this.set('openModal', false); + }, + + closed() { + this.get('bufferedModel').discardChanges(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/server-table.js b/src/api-umbrella/admin-ui/app/components/apis/server-table.js new file mode 100644 index 000000000..28249c1f1 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/server-table.js @@ -0,0 +1,26 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + store: Ember.inject.service(), + openModal: false, + + actions: { + add() { + this.set('serverModel', this.get('store').createRecord('api/server')); + this.set('openModal', true); + }, + + edit(server) { + this.set('serverModel', server); + this.set('openModal', true); + }, + + remove(server) { + bootbox.confirm('Are you sure you want to remove this server?', function(response) { + if(response) { + this.get('model.servers').removeObject(server); + } + }.bind(this)); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/settings/allowed-ips-fields.js b/src/api-umbrella/admin-ui/app/components/apis/settings/allowed-ips-fields.js new file mode 100644 index 000000000..926b61300 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/settings/allowed-ips-fields.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/settings/allowed-referers-fields.js b/src/api-umbrella/admin-ui/app/components/apis/settings/allowed-referers-fields.js new file mode 100644 index 000000000..926b61300 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/settings/allowed-referers-fields.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/settings/common-fields.js b/src/api-umbrella/admin-ui/app/components/apis/settings/common-fields.js new file mode 100644 index 000000000..4550fbf90 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/settings/common-fields.js @@ -0,0 +1,46 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + store: Ember.inject.service(), + + requireHttpsOptions: [ + { id: null, name: I18n.t('admin.api.settings.require_https_options.inherit') }, + { id: 'required_return_error', name: I18n.t('admin.api.settings.require_https_options.required_return_error') }, + { id: 'required_return_redirect', name: I18n.t('admin.api.settings.require_https_options.required_return_redirect') }, + { id: 'transition_return_error', name: I18n.t('admin.api.settings.require_https_options.transition_return_error') }, + { id: 'transition_return_redirect', name: I18n.t('admin.api.settings.require_https_options.transition_return_redirect') }, + { id: 'optional', name: I18n.t('admin.api.settings.require_https_options.optional') }, + ], + + disableApiKeyOptions: [ + { id: null, name: I18n.t('admin.api.settings.disable_api_key_options.inherit') }, + { id: false, name: I18n.t('admin.api.settings.disable_api_key_options.required') }, + { id: true, name: I18n.t('admin.api.settings.disable_api_key_options.disabled') }, + ], + + apiKeyVerificationLevelOptions: [ + { id: null, name: I18n.t('admin.api.settings.api_key_verification_level_options.inherit') }, + { id: 'none', name: I18n.t('admin.api.settings.api_key_verification_level_options.none') }, + { id: 'transition_email', name: I18n.t('admin.api.settings.api_key_verification_level_options.transition_email') }, + { id: 'required_email', name: I18n.t('admin.api.settings.api_key_verification_level_options.required_email') }, + ], + + roleOptions: Ember.computed(function() { + return this.get('store').findAll('api-user-role'); + }), + + passApiKeyOptions: [ + { id: 'header', name: I18n.t('admin.api.settings.pass_api_key_header') }, + { id: 'param', name: I18n.t('admin.api.settings.pass_api_key_param') }, + ], + + anonymousRateLimitBehaviorOptions: [ + { id: 'ip_fallback', name: 'IP Fallback - API key rate limits are applied as IP limits' }, + { id: 'ip_only', name: 'IP Only - API key rate limits are ignored (only IP based limits are applied)' }, + ], + + authenticatedRateLimitBehaviorOptions: [ + { id: 'all', name: 'All Limits - Both API key rate limits and IP based limits are applied' }, + { id: 'api_key_only', name: 'API Key Only - IP based rate limits are ignored (only API key limits are applied)' }, + ], +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/settings/rate-limit-fields.js b/src/api-umbrella/admin-ui/app/components/apis/settings/rate-limit-fields.js new file mode 100644 index 000000000..f4a086bed --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/settings/rate-limit-fields.js @@ -0,0 +1,54 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + store: Ember.inject.service(), + + rateLimitModeOptions: [ + { id: null, name: 'Default rate limits' }, + { id: 'custom', name: 'Custom rate limits' }, + { id: 'unlimited', name: 'Unlimited requests' }, + ], + + rateLimitDurationUnitOptions: [ + { id: 'seconds', name: 'seconds' }, + { id: 'minutes', name: 'minutes' }, + { id: 'hours', name: 'hours' }, + { id: 'days', name: 'days' }, + ], + + rateLimitLimitByOptions: [ + { id: 'apiKey', name: 'API Key' }, + { id: 'ip', name: 'IP Address' }, + ], + + uniqueSettingsId: Ember.computed(function() { + return _.uniqueId('api_settings_'); + }), + + actions: { + primaryRateLimitChange(selectedRateLimit) { + let rateLimits = this.get('model.rateLimits'); + rateLimits.forEach(function(rateLimit) { + if(rateLimit === selectedRateLimit) { + rateLimit.set('responseHeaders', true); + } else { + rateLimit.set('responseHeaders', false); + } + }); + }, + + addRateLimit() { + let collection = this.get('model.rateLimits'); + collection.pushObject(this.get('store').createRecord('api/rate-limit')); + }, + + deleteRateLimit(rateLimit) { + let collection = this.get('model.rateLimits'); + bootbox.confirm('Are you sure you want to remove this rate limit?', function(result) { + if(result) { + collection.removeObject(rateLimit); + } + }); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/sub-settings-form.js b/src/api-umbrella/admin-ui/app/components/apis/sub-settings-form.js new file mode 100644 index 000000000..42082ce3c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/sub-settings-form.js @@ -0,0 +1,45 @@ +import Ember from 'ember'; +import BufferedProxy from 'ember-buffered-proxy/proxy'; + +export default Ember.Component.extend({ + openModal: false, + httpMethodOptions: [ + { id: 'any', name: 'Any' }, + { id: 'GET', name: 'GET' }, + { id: 'POST', name: 'POST' }, + { id: 'PUT', name: 'PUT' }, + { id: 'DELETE', name: 'DELETE' }, + { id: 'HEAD', name: 'HEAD' }, + { id: 'TRACE', name: 'TRACE' }, + { id: 'OPTIONS', name: 'OPTIONS' }, + { id: 'CONNECT', name: 'CONNECT' }, + { id: 'PATCH', name: 'PATCH' }, + ], + + modalTitle: Ember.computed('model', function() { + if(this.get('model.isNew')) { + return 'Add Sub-URL Request Settings'; + } else { + return 'Edit Sub-URL Request Settings'; + } + }), + + bufferedModel: Ember.computed('model', function() { + return BufferedProxy.create({ content: this.get('model') }); + }), + + actions: { + submit() { + this.get('bufferedModel').applyChanges(); + if(this.get('model.isNew')) { + this.get('collection').pushObject(this.get('model')); + } + + this.set('openModal', false); + }, + + closed() { + this.get('bufferedModel').discardChanges(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/sub-settings-table.js b/src/api-umbrella/admin-ui/app/components/apis/sub-settings-table.js new file mode 100644 index 000000000..1c7954be3 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/sub-settings-table.js @@ -0,0 +1,32 @@ +import Ember from 'ember'; +import Sortable from 'api-umbrella-admin-ui/mixins/sortable'; + +export default Ember.Component.extend(Sortable, { + store: Ember.inject.service(), + openModal: false, + + sortableCollection: Ember.computed('model', function() { + return this.get('model.subSettings'); + }), + + actions: { + add() { + this.set('subSettingsModel', this.get('store').createRecord('api/sub-settings')); + this.set('openModal', true); + }, + + edit(subSettings) { + this.set('subSettingsModel', subSettings); + this.set('openModal', true); + }, + + remove(subSettings) { + bootbox.confirm('Are you sure you want to remove this URL setting?', function(response) { + if(response) { + this.get('model.subSettings').removeObject(subSettings); + } + }.bind(this)); + }, + }, + +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/url-match-form.js b/src/api-umbrella/admin-ui/app/components/apis/url-match-form.js new file mode 100644 index 000000000..d9f3bcc03 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/url-match-form.js @@ -0,0 +1,48 @@ +import Ember from 'ember'; +import BufferedProxy from 'ember-buffered-proxy/proxy'; +import UrlMatch from 'api-umbrella-admin-ui/models/api/url-match'; + +export default Ember.Component.extend({ + openModal: false, + exampleSuffix: 'example.json?param=value', + + modalTitle: Ember.computed('model', function() { + if(this.get('model.isNew')) { + return 'Add Matching URL Prefix'; + } else { + return 'Edit Matching URL Prefix'; + } + }), + + bufferedModel: Ember.computed('model', function() { + let owner = Ember.getOwner(this).ownerInjection(); + return BufferedProxy.extend(UrlMatch.validationClass).create(owner, { content: this.get('model') }); + }), + + exampleIncomingUrl: Ember.computed('bufferedModel.frontendPrefix', function() { + let root = this.get('apiExampleIncomingUrlRoot') || ''; + let prefix = this.get('bufferedModel.frontendPrefix') || ''; + return root + prefix + this.get('exampleSuffix'); + }), + + exampleOutgoingUrl: Ember.computed('bufferedModel.frontendPrefix', 'bufferedModel.backendPrefix', function() { + let root = this.get('apiExampleIncomingUrlRoot') || ''; + let prefix = this.get('bufferedModel.backendPrefix') || this.get('bufferedModel.frontendPrefix') || ''; + return root + prefix + this.get('exampleSuffix'); + }), + + actions: { + submit() { + this.get('bufferedModel').applyChanges(); + if(this.get('model.isNew')) { + this.get('collection').pushObject(this.get('model')); + } + + this.set('openModal', false); + }, + + closed() { + this.get('bufferedModel').discardChanges(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/apis/url-match-table.js b/src/api-umbrella/admin-ui/app/components/apis/url-match-table.js new file mode 100644 index 000000000..07d103af1 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/apis/url-match-table.js @@ -0,0 +1,31 @@ +import Ember from 'ember'; +import Sortable from 'api-umbrella-admin-ui/mixins/sortable'; + +export default Ember.Component.extend(Sortable, { + store: Ember.inject.service(), + openModal: false, + + sortableCollection: Ember.computed('model', function() { + return this.get('model.urlMatches'); + }), + + actions: { + add() { + this.set('urlMatchModel', this.get('store').createRecord('api/url-match')); + this.set('openModal', true); + }, + + edit(urlMatch) { + this.set('urlMatchModel', urlMatch); + this.set('openModal', true); + }, + + remove(urlMatch) { + bootbox.confirm('Are you sure you want to remove this URL prefix?', function(response) { + if(response) { + this.get('model.urlMatches').removeObject(urlMatch); + } + }.bind(this)); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/config/publish-form-records.js b/src/api-umbrella/admin-ui/app/components/config/publish-form-records.js new file mode 100644 index 000000000..b891cc57a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/config/publish-form-records.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + actions: { + toggleConfigDiff(id) { + $('[data-diff-id=' + id + ']').toggle(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/config/publish-form.js b/src/api-umbrella/admin-ui/app/components/config/publish-form.js new file mode 100644 index 000000000..e65afff9b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/config/publish-form.js @@ -0,0 +1,132 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + routing: Ember.inject.service('-routing'), + + didInsertElement() { + this.$submitButton = $('#publish_button'); + this.$toggleCheckboxesLink = $('#toggle_checkboxes'); + $('#publish_form').on('change', ':checkbox', _.bind(this.onCheckboxChange, this)); + + let $checkboxes = $('#publish_form :checkbox'); + if($checkboxes.length === 1) { + $checkboxes.prop('checked', true); + } + + this.onCheckboxChange(); + + this.$().find('.diff-active-yaml').each(function() { + let activeYaml = $(this).text(); + let pendingYaml = $(this).siblings('.diff-pending-yaml').text(); + + let diff = JsDiff.diffWords(activeYaml, pendingYaml); + + let fragment = document.createDocumentFragment(); + for(let i = 0; i < diff.length; i++) { + if(diff[i].added && diff[i + 1] && diff[i + 1].removed) { + let swap = diff[i]; + diff[i] = diff[i + 1]; + diff[i + 1] = swap; + } + + let node; + if(diff[i].removed) { + node = document.createElement('del'); + node.appendChild(document.createTextNode(diff[i].value)); + } else if(diff[i].added) { + node = document.createElement('ins'); + node.appendChild(document.createTextNode(diff[i].value)); + } else { + node = document.createTextNode(diff[i].value); + } + + fragment.appendChild(node); + } + + let diffOutput = $(this).siblings('.config-diff'); + diffOutput.html(fragment); + }); + }, + + onCheckboxChange() { + let $unchecked = $('#publish_form :checkbox:not(:checked)'); + if($unchecked.length > 0) { + this.$toggleCheckboxesLink.text(this.$toggleCheckboxesLink.data('check-all')); + } else { + this.$toggleCheckboxesLink.text(this.$toggleCheckboxesLink.data('uncheck-all')); + } + + let $checked = $('#publish_form :checkbox:checked'); + if($checked.length > 0) { + this.$submitButton.prop('disabled', false); + } else { + this.$submitButton.prop('disabled', true); + } + }, + + hasChanges: Ember.computed('model.config.apis.new.@each', 'model.config.apis.modified.@each', 'model.config.apis.deleted.@each', 'model.config.website_backends.new.@each', 'model.config.website_backends.modified.@each', 'model.config.website_backends.deleted.@each', function() { + let newApis = this.get('model.config.apis.new'); + let modifiedApis = this.get('model.config.apis.modified'); + let deletedApis = this.get('model.config.apis.deleted'); + let newWebsiteBackends = this.get('model.config.website_backends.new'); + let modifiedWebsiteBackends = this.get('model.config.website_backends.modified'); + let deletedWebsiteBackends = this.get('model.config.website_backends.deleted'); + + if(newApis.length > 0 || modifiedApis.length > 0 || deletedApis.length > 0 || newWebsiteBackends.length > 0 || modifiedWebsiteBackends.length > 0 || deletedWebsiteBackends.length > 0) { + return true; + } else { + return false; + } + }), + + actions: { + toggleAllCheckboxes() { + let $checkboxes = $('#publish_form :checkbox'); + let $unchecked = $('#publish_form :checkbox').not(':checked'); + + if($unchecked.length > 0) { + $checkboxes.prop('checked', true); + } else { + $checkboxes.prop('checked', false); + } + + this.onCheckboxChange(); + }, + + publish() { + let form = $('#publish_form'); + + let button = $('#publish_button'); + button.button('loading'); + + $.ajax({ + url: '/api-umbrella/v1/config/publish', + type: 'POST', + data: form.serialize(), + }).then(_.bind(function() { + button.button('reset'); + new PNotify({ + type: 'success', + title: 'Published', + text: 'Successfully published the configuration
Changes should be live in a few seconds...', + }); + + this.get('routing.router.router').refresh(); + }, this), function(response) { + let message = '

Error

'; + try { + let errors = response.responseJSON.errors; + for(let prop in errors) { + message += prop + ': ' + errors[prop].join(', ') + '
'; + } + } catch(e) { + message = 'An unexpected error occurred: ' + response.responseText; + } + + button.button('reset'); + Ember.Logger.error(message); + bootbox.alert(message); + }); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/error-messages.js b/src/api-umbrella/admin-ui/app/components/error-messages.js new file mode 100644 index 000000000..d7c464234 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/error-messages.js @@ -0,0 +1,75 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + messages: Ember.computed('model.clientErrors', 'model.serverErrors', function() { + let errors = []; + + let clientErrors = this.get('model.clientErrors'); + if(clientErrors) { + if(_.isArray(clientErrors)) { + _.each(clientErrors, function(clientError) { + let message = clientError.get('message'); + if(message) { + errors.push({ + attribute: clientError.get('attribute'), + message: message, + }); + } else { + errors.push({ message: 'Unexpected error' }); + } + }); + } else { + errors.push({ message: 'Unexpected error' }); + } + } + + let serverErrors = this.get('model.serverErrors'); + if(serverErrors) { + if(_.isArray(serverErrors)) { + _.each(serverErrors, function(serverError) { + let message = serverError.message; + if(!message && serverError.title) { + message = serverError.title; + if(serverError.status) { + message += ' (Status: ' + serverError.status + ')'; + } + } + + if(message) { + errors.push({ + attribute: serverError.field, + message: message, + }); + } else { + errors.push({ message: 'Unexpected error' }); + } + }); + } else { + errors.push({ message: 'Unexpected error' }); + } + } + + let messages = []; + _.each(errors, function(error) { + let message = ''; + if(error.attribute && error.attribute !== 'base') { + message += inflection.titleize(inflection.underscore(error.attribute)) + ': '; + message += error.message || 'Unexpected error'; + } else { + if(error.message) { + message += error.message.charAt(0).toUpperCase() + error.message.slice(1); + } else { + message += 'Unexpected error'; + } + } + + messages.push(marked(message)); + }); + + return messages; + }), + + hasErrors: Ember.computed('messages', function() { + return (this.get('messages').length > 0); + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/fields-for.js b/src/api-umbrella/admin-ui/app/components/fields-for.js new file mode 100644 index 000000000..926b61300 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/fields-for.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/ace-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/ace-field.js new file mode 100644 index 000000000..07a08f2d6 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/ace-field.js @@ -0,0 +1,57 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ + init() { + this._super(); + this.set('aceId', this.get('elementId') + '_ace'); + this.set('aceTextInputId', this.get('elementId') + '_ace_text_input'); + this.addObserver('model.' + this.get('fieldName'), this, this.valueDidChange); + }, + + didInsertElement() { + this._super(); + + let aceId = this.get('aceId'); + let $element = this.$().find('textarea'); + $element.hide(); + $element.before('
'); + + this.editor = ace.edit(aceId); + + let editor = this.editor; + let session = this.editor.getSession(); + + editor.$blockScrolling = Infinity; + editor.setTheme('ace/theme/textmate'); + editor.setShowPrintMargin(false); + editor.setHighlightActiveLine(false); + session.setUseWorker(false); + session.setTabSize(2); + session.setMode('ace/mode/' + $element.data('ace-mode')); + session.setValue($element.val()); + + let $textElement = $(editor.textInput.getElement()); + $textElement.attr('id', this.get('aceTextInputId')); + $textElement.attr('data-raw-input-id', $element.attr('id')); + + let contentId = this.get('elementId') + '_ace_content'; + let $content = $(editor.container).find('.ace_content'); + $content.attr('id', contentId); + $textElement.attr('data-ace-content-id', contentId); + + session.on('change', function() { + $element.val(session.getValue()); + $element.trigger('change'); + }); + }, + + valueDidChange() { + if(this.editor) { + let session = this.editor.getSession(); + let value = this.get('model.' + this.get('fieldName')); + if(value !== session.getValue()) { + session.setValue(value); + } + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/base-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/base-field.js new file mode 100644 index 000000000..36f57f43c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/base-field.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + inputId: Ember.computed('elementId', 'fieldName', function() { + return this.get('elementId') + '-' + this.get('fieldName'); + }), +}).reopenClass({ + positionalParams: ['fieldName'], +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/checkbox-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/checkbox-field.js new file mode 100644 index 000000000..16a921a76 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/checkbox-field.js @@ -0,0 +1,4 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/checkboxes-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/checkboxes-field.js new file mode 100644 index 000000000..16a921a76 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/checkboxes-field.js @@ -0,0 +1,4 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/error-messages.js b/src/api-umbrella/admin-ui/app/components/form-fields/error-messages.js new file mode 100644 index 000000000..926b61300 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/error-messages.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/field-wrapper.js b/src/api-umbrella/admin-ui/app/components/form-fields/field-wrapper.js new file mode 100644 index 000000000..9970e1441 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/field-wrapper.js @@ -0,0 +1,55 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + canShowErrors: false, + + labelFor: Ember.computed('labelForId', 'inputId', function() { + return this.get('labelForId') || this.get('inputId'); + }), + + fieldNameDidChange: Ember.on('init', Ember.observer('fieldName', function() { + let fieldName = this.get('fieldName'); + let fieldValidations = 'model.validations.attrs.' + fieldName; + Ember.mixin(this, { + fieldErrorMessages: Ember.computed(fieldValidations + '.messages', 'canShowErrors', function() { + if(this.get('canShowErrors')) { + return this.get(fieldValidations + '.messages'); + } else { + return []; + } + }), + fieldHasErrors: Ember.computed(fieldValidations + '.isValid', 'canShowErrors', function() { + if(this.get('canShowErrors')) { + return (this.get(fieldValidations + '.isValid') === false); + } else { + return false; + } + }), + }); + })), + + wrapperErrorClass: Ember.computed('fieldHasErrors', function() { + if(this.get('fieldHasErrors')) { + return 'has-error'; + } else { + return ''; + } + }), + + // Don't show errors until the field has been unfocused. This prevents all + // the inline errors from showing up on initial render. + focusOut() { + this.set('canShowErrors', true); + }, + + // Anytime the model changes, reset the error display so errors aren't + // displayed until the field is unfocused again. + // + // This helps handle modals where the same form might be reused multiple + // times. Without this, errors would show up immediately the second time the + // modal is opened if all the fields were unfocused the first time the modal + // was opened. + hideErrorsOnModelChange: Ember.observer('model', function() { + this.set('canShowErrors', false); + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/select-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/select-field.js new file mode 100644 index 000000000..16a921a76 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/select-field.js @@ -0,0 +1,4 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/selectize-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/selectize-field.js new file mode 100644 index 000000000..9afa2c516 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/selectize-field.js @@ -0,0 +1,93 @@ +import BaseField from './base-field'; +import Ember from 'ember'; + +export default BaseField.extend({ + optionValuePath: 'id', + optionLabelPath: 'id', + defaultOptions: [], + + init() { + this._super(); + this.set('selectizeTextInputId', this.get('elementId') + '-selectize_text_input'); + this.addObserver('model.' + this.get('fieldName'), this, this.valueDidChange); + }, + + didInsertElement() { + this._super(); + + this.$input = this.$().find('#' + this.get('inputId')).selectize({ + plugins: ['restore_on_backspace', 'remove_button'], + delimiter: ',', + options: this.get('defaultOptions'), + valueField: 'id', + labelField: 'label', + searchField: 'label', + sortField: 'label', + create: true, + + // Add to body so it doesn't get clipped by parent div containers. + dropdownParent: 'body', + }); + + this.selectize = this.$input[0].selectize; + this.selectize.$control_input.attr('id', this.get('selectizeTextInputId')); + this.selectize.$control_input.attr('data-raw-input-id', this.get('inputId')); + + let controlId = this.get('elementId') + '-selectize_control'; + this.selectize.$control.attr('id', controlId); + this.selectize.$control_input.attr('data-selectize-control-id', controlId); + }, + + defaultOptionsDidChange: Ember.on('init', Ember.observer('options.@each', function() { + this.set('defaultOptions', this.get('options').map(_.bind(function(item) { + return { + id: item.get(this.get('optionValuePath')), + label: item.get(this.get('optionLabelPath')), + }; + }, this))); + + if(this.selectize) { + this.get('defaultOptions').forEach(_.bind(function(option) { + this.selectize.addOption(option); + }, this)); + + this.selectize.refreshOptions(false); + } + })), + + // Sync the selectize input with the value binding if the value changes + // externally. + valueDidChange() { + if(this.selectize) { + let valueString = this.get('model.' + this.get('fieldName')); + if(valueString !== this.selectize.getValue()) { + let values = valueString; + if(values) { + values = _.uniq(values.split(',')); + + // Ensure the selected value is available as an option in the menu. + // This takes into account the fact that the default options may not + // be loaded yet, or they may not contain this specific option. + for(let i = 0; i < values.length; i++) { + let option = { + id: values[i], + label: values[i], + }; + + this.selectize.addOption(option); + } + + this.selectize.refreshOptions(false); + } + + this.selectize.setValue(values); + } + } + }, + + willDestroyElement() { + if(this.selectize) { + this.selectize.destroy(); + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/static-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/static-field.js new file mode 100644 index 000000000..16a921a76 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/static-field.js @@ -0,0 +1,4 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/text-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/text-field.js new file mode 100644 index 000000000..16a921a76 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/text-field.js @@ -0,0 +1,4 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/textarea-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/textarea-field.js new file mode 100644 index 000000000..16a921a76 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/textarea-field.js @@ -0,0 +1,4 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/help-tooltip.js b/src/api-umbrella/admin-ui/app/components/help-tooltip.js new file mode 100644 index 000000000..ced037466 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/help-tooltip.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: 'span', +}); diff --git a/src/api-umbrella/admin-ui/app/components/select-menu.js b/src/api-umbrella/admin-ui/app/components/select-menu.js new file mode 100644 index 000000000..9e241568e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/select-menu.js @@ -0,0 +1,27 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + // If a select menu doesn't have a value set on the model, set it to the + // value of the first option. This better aligns with the default behavior of + // select menus (so even if the user doesn't interact with the menu, the + // model still gets set with the first value that will always be selected). + // + // We do this differently than the emberx-select way here: + // https://github.com/thefrontside/emberx-select/pull/90 + // Instead, we do this with an observer on any value changes. This is needed + // for select menus inside our modals to work, since the model on those isn't + // set until the modal opens (so setting a default value just on the initial + // render doesn't work). + updateDefault: Ember.on('init', Ember.observer('value', function() { + let value = this.get('value'); + if(value === undefined) { + let options = this.get('options'); + if(options) { + let firstOption = options[0]; + if(firstOption && firstOption.id) { + this.sendAction('action', firstOption.id, this); + } + } + } + })), +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-breadcrumbs.js b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-breadcrumbs.js new file mode 100644 index 000000000..13edcfb64 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-breadcrumbs.js @@ -0,0 +1,23 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + breadcrumbLinks: Ember.computed('breadcrumbs', function() { + let crumbs = []; + + let data = this.get('breadcrumbs'); + for(let i = 0; i < data.length; i++) { + let crumb = { name: data[i].crumb }; + if(i < data.length - 1) { + crumb.prefix = data[i].prefix; + } + + crumbs.push(crumb); + } + + if(crumbs.length <= 1) { + crumbs = []; + } + + return crumbs; + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-chart.js b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-chart.js new file mode 100644 index 000000000..428a9769e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-chart.js @@ -0,0 +1,101 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + chartOptions: { + pointSize: 0, + lineWidth: 1, + focusTarget: 'category', + width: '100%', + chartArea: { + width: '95%', + height: '88%', + top: 0, + }, + fontSize: 12, + isStacked: true, + areaOpacity: 0.2, + vAxis: { + gridlines: { + count: 4, + }, + textStyle: { + fontSize: 11, + }, + textPosition: 'in', + }, + hAxis: { + format: 'MMM d', + baselineColor: 'transparent', + gridlines: { + color: 'transparent', + }, + }, + legend: { + position: 'none', + }, + }, + + chartData: { + cols: [ + {id: 'date', label: 'Date', type: 'datetime'}, + {id: 'hits', label: 'Hits', type: 'number'}, + ], + rows: [], + }, + + didInsertElement() { + google.charts.setOnLoadCallback(this.renderChart.bind(this)); + }, + + renderChart() { + this.chart = new google.visualization.AreaChart(this.$()[0]); + + // On first load, refresh the data. Afterwards the observer should handle + // refreshing. + if(!this.dataTable) { + this.refreshData(); + } + + $(window).on('resize', _.debounce(this.draw.bind(this), 100)); + }, + + refreshData: Ember.observer('hitsOverTime', function() { + // Defer until Google Charts is loaded if this got called earlier from the + // observer. + if(!google || !google.visualization || !google.visualization.DataTable) { + return; + } + + this.chartData = this.get('hitsOverTime'); + for(let i = 0; i < this.chartData.rows.length; i++) { + this.chartData.rows[i].c[0].v = new Date(this.chartData.rows[i].c[0].v); + } + + // Show hours on the axis when viewing minutely date. + switch(this.get('controller.query.params.interval')) { + case 'minute': + this.chartOptions.hAxis.format = 'MMM d h a'; + break; + default: + this.chartOptions.hAxis.format = 'MMM d'; + break; + } + + // Show hours on the axis when viewing less than 2 days of hourly data. + if(this.get('controller.query.params.interval') === 'hour') { + let start = moment(this.get('controller.query.params.start_at')); + let end = moment(this.get('controller.query.params.end_at')); + let maxDuration = 2 * 24 * 60 * 60; // 2 days + if(end.unix() - start.unix() <= maxDuration) { + this.chartOptions.hAxis.format = 'MMM d h a'; + } + } + + this.dataTable = new google.visualization.DataTable(this.chartData); + this.draw(); + }), + + draw() { + this.chart.draw(this.dataTable, this.chartOptions); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-table.js b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-table.js new file mode 100644 index 000000000..6f89aa1a9 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-table.js @@ -0,0 +1,59 @@ +import Ember from 'ember'; +import numeral from 'numeral'; + +export default Ember.Component.extend({ + session: Ember.inject.service(), + + didInsertElement() { + this.$().find('table').DataTable({ + searching: false, + order: [[1, 'desc']], + data: this.get('results'), + columns: [ + { + data: 'path', + title: 'Path', + defaultContent: '-', + render: function(name, type, data) { + if(type === 'display' && name && name !== '-') { + if(data.terminal) { + return '' + _.escape(name); + } else { + let params = _.clone(this.get('queryParamValues')); + params.prefix = data.descendent_prefix; + let link = '#/stats/drilldown?' + $.param(params); + + return '' + _.escape(name) + ''; + } + } + + return name; + }.bind(this), + }, + { + data: 'hits', + title: 'Hits', + defaultContent: '-', + render(number, type) { + if(type === 'display' && number && number !== '-') { + return numeral(number).format('0,0'); + } + + return number; + }, + }, + ], + }); + }, + + refreshData: Ember.observer('results', function() { + let table = this.$().find('table').dataTable().api(); + table.clear(); + table.rows.add(this.get('results')); + table.draw(); + }), + + downloadUrl: Ember.computed('allQueryParamValues', function() { + return '/api-umbrella/v1/analytics/drilldown.csv?api_key=' + this.get('session.data.authenticated.api_key') + '&' + $.param(this.get('allQueryParamValues')); + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/logs/results-chart.js b/src/api-umbrella/admin-ui/app/components/stats/logs/results-chart.js new file mode 100644 index 000000000..5d2b4f1ba --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/logs/results-chart.js @@ -0,0 +1,107 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + chartOptions: { + focusTarget: 'category', + width: '100%', + chartArea: { + width: '95%', + height: '88%', + top: 0, + }, + fontSize: 12, + colors: ['#4682B4'], + areaOpacity: 0.2, + vAxis: { + gridlines: { + count: 4, + }, + textStyle: { + fontSize: 11, + }, + textPosition: 'in', + }, + hAxis: { + format: 'MMM d', + baselineColor: 'transparent', + gridlines: { + color: 'transparent', + }, + }, + legend: { + position: 'none', + }, + }, + + chartData: { + cols: [ + {id: 'date', label: 'Date', type: 'datetime'}, + {id: 'hits', label: 'Hits', type: 'number'}, + ], + rows: [], + }, + + didInsertElement() { + google.charts.setOnLoadCallback(this.renderChart.bind(this)); + }, + + renderChart() { + this.chart = new google.visualization.AreaChart(this.$()[0]); + + // On first load, refresh the data. Afterwards the observer should handle + // refreshing. + if(!this.dataTable) { + this.refreshData(); + } + + $(window).on('resize', _.debounce(this.draw.bind(this), 100)); + }, + + refreshData: Ember.observer('hitsOverTime', function() { + // Defer until Google Charts is loaded if this got called earlier from the + // observer. + if(!google || !google.visualization || !google.visualization.DataTable) { + return; + } + + this.chartData.rows = this.get('hitsOverTime') || []; + for(let i = 0; i < this.chartData.rows.length; i++) { + this.chartData.rows[i].c[0].v = new Date(this.chartData.rows[i].c[0].v); + } + + if(this.chartData.rows.length < 100) { + this.chartOptions.pointSize = 8; + this.chartOptions.lineWidth = 4; + } else { + this.chartOptions.pointSize = 0; + this.chartOptions.lineWidth = 3; + } + + // Show hours on the axis when viewing minutely date. + switch(this.get('controller.query.params.interval')) { + case 'minute': + this.chartOptions.hAxis.format = 'MMM d h a'; + break; + default: + this.chartOptions.hAxis.format = 'MMM d'; + break; + } + + // Show hours on the axis when viewing less than 2 days of hourly data. + if(this.get('controller.query.params.interval') === 'hour') { + let start = moment(this.get('controller.query.params.start_at')); + let end = moment(this.get('controller.query.params.end_at')); + let maxDuration = 2 * 24 * 60 * 60; // 2 days + if(end.unix() - start.unix() <= maxDuration) { + this.chartOptions.hAxis.format = 'MMM d h a'; + } + } + + this.dataTable = new google.visualization.DataTable(this.chartData); + this.draw(); + }), + + draw() { + this.chart.draw(this.dataTable, this.chartOptions); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/logs/results-facet-table.js b/src/api-umbrella/admin-ui/app/components/stats/logs/results-facet-table.js new file mode 100644 index 000000000..3dac5d1a5 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/logs/results-facet-table.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + setLinks: Ember.on('init', Ember.observer('facets', function() { + _.each(this.get('facets'), function(bucket) { + let params = _.clone(this.get('queryParamValues')); + params.search = _.compact([params.search, this.get('field') + ':"' + bucket.key + '"']).join(' AND '); + bucket.link = '#/stats/logs?' + $.param(params); + }.bind(this)); + })), + + actions: { + toggleFacetTable() { + this.$().find('table').toggle(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/logs/results-highlights.js b/src/api-umbrella/admin-ui/app/components/stats/logs/results-highlights.js new file mode 100644 index 000000000..926b61300 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/logs/results-highlights.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/logs/results-table.js b/src/api-umbrella/admin-ui/app/components/stats/logs/results-table.js new file mode 100644 index 000000000..038c0b14c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/logs/results-table.js @@ -0,0 +1,184 @@ +import Ember from 'ember'; +import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; + +export default Ember.Component.extend({ + didInsertElement() { + this.$().find('table').DataTable({ + searching: false, + serverSide: true, + ajax: { + url: '/admin/stats/logs.json', + // Use POST for this endpoint, since the URLs can be very long and + // exceed URL length limits in IE (and apparently Capybara too). + type: 'POST', + data: function(data) { + return _.extend({}, data, this.get('allQueryParamValues')); + }.bind(this), + }, + drawCallback: _.bind(function() { + this.$().find('td').truncate({ + width: 400, + addtitle: true, + addclass: 'truncated', + }); + + this.$().find('.truncated').qtip({ + style: { + classes: 'qtip-bootstrap qtip-forced-wide', + }, + hide: { + fixed: true, + delay: 200, + }, + position: { + viewport: false, + my: 'bottom center', + at: 'top center', + }, + }); + }, this), + order: [[0, 'desc']], + columns: [ + { + data: 'request_at', + type: 'date', + title: 'Time', + defaultContent: '-', + render: DataTablesHelpers.renderTime, + }, + { + data: 'request_method', + title: 'Method', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_host', + title: 'Host', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_url', + title: 'URL', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'user_email', + title: 'User', + defaultContent: '-', + render: function(email, type, data) { + if(type === 'display' && email && email !== '-') { + let params = _.clone(this.get('queryParamValues')); + params.search = _.compact([params.search, 'user_id:"' + data.user_id + '"']).join(' AND '); + let link = '#/stats/logs?' + $.param(params); + + return '' + _.escape(email) + ''; + } + + return email; + }.bind(this), + }, + { + data: 'request_ip', + title: 'IP Address', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_ip_country', + title: 'Country', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_ip_region', + title: 'State', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_ip_city', + title: 'City', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'response_status', + title: 'Status', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'gatekeeper_denied_code', + title: 'Reason Denied', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'response_time', + title: 'Response Time', + defaultContent: '-', + render(time, type) { + if(type === 'display' && time && time !== '-') { + return time + ' ms'; + } + + return time; + }, + }, + { + data: 'response_content_type', + title: 'Content Type', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_accept_encoding', + title: 'Accept Encoding', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_user_agent', + title: 'User Agent', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_user_agent_family', + title: 'User Agent Family', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_user_agent_type', + title: 'User Agent Type', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_referer', + title: 'Referer', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'request_origin', + title: 'Origin', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + ], + }); + }, + + refreshData: Ember.observer('allQueryParamValues', function() { + this.$().find('table').DataTable().draw(); + }), + + downloadUrl: Ember.computed('allQueryParamValues', function() { + return '/admin/stats/logs.csv?' + $.param(this.get('allQueryParamValues')); + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/map/results-breadcrumbs.js b/src/api-umbrella/admin-ui/app/components/stats/map/results-breadcrumbs.js new file mode 100644 index 000000000..ef520aca9 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/map/results-breadcrumbs.js @@ -0,0 +1,23 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + breadcrumbLinks: Ember.computed('breadcrumbs', function() { + let crumbs = []; + + let data = this.get('breadcrumbs'); + for(let i = 0; i < data.length; i++) { + let crumb = { name: data[i].name }; + if(i < data.length - 1) { + crumb.region = data[i].region; + } + + crumbs.push(crumb); + } + + if(crumbs.length <= 1) { + crumbs = []; + } + + return crumbs; + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/map/results-map.js b/src/api-umbrella/admin-ui/app/components/stats/map/results-map.js new file mode 100644 index 000000000..a9e7b3784 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/map/results-map.js @@ -0,0 +1,91 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + chartOptions: { + width: 640, + colorAxis: { + colors: ['#B0DBFF', '#4682B4'], + }, + }, + + chartData: { + cols: [], + rows: [], + }, + + didInsertElement() { + google.charts.setOnLoadCallback(this.renderChart.bind(this)); + }, + + renderChart() { + this.chart = new google.visualization.GeoChart(this.$()[0]); + google.visualization.events.addListener(this.chart, 'regionClick', _.bind(this.handleRegionClick, this)); + google.visualization.events.addListener(this.chart, 'select', _.bind(this.handleCityClick, this)); + + // On first load, refresh the data. Afterwards the observer should handle + // refreshing. + if(!this.dataTable) { + this.refreshData(); + } + + $(window).on('resize', _.debounce(this.draw.bind(this), 100)); + }, + + handleRegionClick(region) { + this.set('controller.query.params.region', region.region); + }, + + handleCityClick() { + if(this.get('regionField') === 'request_ip_city') { + let selection = this.chart.getSelection(); + if(selection) { + let rowIndex = selection[0].row; + let region = this.dataTable.getValue(rowIndex, 2); + + let params = _.clone(this.get('controller.query.params')); + params.search = 'request_ip_city:"' + region + '"'; + let router = this.get('controller.target.router'); + router.transitionTo('stats.logs', $.param(params)); + } + } + }, + + refreshData: Ember.observer('regions', function() { + // Defer until Google Charts is loaded if this got called earlier from the + // observer. + if(!google || !google.visualization || !google.visualization.DataTable) { + return; + } + + this.chartData.rows = this.get('regions') || []; + this.chartData.cols = [ + {id: 'region', label: 'Region', type: 'string'}, + {id: 'startDate', label: 'Hits', type: 'number'}, + ]; + + if(this.get('regionField') === 'request_ip_city') { + this.chartData.cols.unshift({id: 'latitude', label: 'Latitude', type: 'number'}, + {id: 'longitude', label: 'Longitude', type: 'number'}); + } + + this.chartOptions.region = this.get('allQueryParamValues.region'); + if(this.chartOptions.region.indexOf('US') === 0) { + this.chartOptions.resolution = 'provinces'; + } else { + this.chartOptions.resolution = 'countries'; + } + + if(this.chartOptions.region === 'world' || this.chartOptions.region === 'US') { + this.chartOptions.displayMode = 'regions'; + } else { + this.chartOptions.displayMode = 'markers'; + } + + this.dataTable = new google.visualization.DataTable(this.chartData); + this.draw(); + }), + + draw() { + this.chart.draw(this.dataTable, this.chartOptions); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/map/results-table.js b/src/api-umbrella/admin-ui/app/components/stats/map/results-table.js new file mode 100644 index 000000000..2e418fa05 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/map/results-table.js @@ -0,0 +1,60 @@ +import Ember from 'ember'; +import numeral from 'numeral'; + +export default Ember.Component.extend({ + didInsertElement() { + this.$().find('table').DataTable({ + searching: false, + order: [[1, 'desc']], + data: this.get('regions'), + columns: [ + { + data: 'name', + title: 'Location', + defaultContent: '-', + render: _.bind(function(name, type, data) { + if(type === 'display' && name && name !== '-') { + let link; + let params = _.clone(this.get('queryParamValues')); + if(this.get('regionField') === 'request_ip_city') { + delete params.region; + params.search = 'request_ip_city:"' + data.id + '"'; + link = '#/stats/logs?' + $.param(params); + } else { + params.region = data.id; + link = '#/stats/map?' + $.param(params); + } + + return '' + _.escape(name) + ''; + } + + return name; + }, this), + }, + { + data: 'hits', + title: 'Hits', + defaultContent: '-', + render(number, type) { + if(type === 'display' && number && number !== '-') { + return numeral(number).format('0,0'); + } + + return number; + }, + }, + ], + }); + }, + + refreshData: Ember.observer('regions', function() { + let table = this.$().find('table').dataTable().api(); + table.clear(); + table.rows.add(this.get('regions')); + table.draw(); + }), + + downloadUrl: Ember.computed('allQueryParamValues', function() { + return '/admin/stats/map.csv?' + $.param(this.get('allQueryParamValues')); + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/query-form.js b/src/api-umbrella/admin-ui/app/components/stats/query-form.js new file mode 100644 index 000000000..8a42d1145 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/query-form.js @@ -0,0 +1,343 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + session: Ember.inject.service('session'), + + enableInterval: false, + + datePickerRanges: { + 'Today': [ + moment().startOf('day'), + moment().endOf('day'), + ], + 'Yesterday': [ + moment().subtract(1, 'days'), + moment().subtract(1, 'days').endOf('day'), + ], + 'Last 7 Days': [ + moment().subtract(6, 'days'), + moment().endOf('day'), + ], + 'Last 30 Days': [ + moment().subtract(29, 'days').startOf('day'), + moment().endOf('day'), + ], + 'This Month': [ + moment().startOf('month'), + moment().endOf('month'), + ], + 'Last Month': [ + moment().subtract(1, 'month').startOf('month'), + moment().subtract(1, 'month').endOf('month'), + ], + }, + + didInsertElement() { + this.updateDateRange(); + + $('#reportrange').daterangepicker({ + ranges: this.datePickerRanges, + startDate: moment(this.get('start_at'), 'YYYY-MM-DD'), + endDate: moment(this.get('end_at'), 'YYYY-MM-DD'), + }, _.bind(this.handleDateRangeChange, this)); + + let stringOperators = [ + 'begins_with', + 'not_begins_with', + 'equal', + 'not_equal', + 'contains', + 'not_contains', + 'is_null', + 'is_not_null', + ]; + + let selectOperators = [ + 'equal', + 'not_equal', + 'is_null', + 'is_not_null', + ]; + + let numberOperators = [ + 'equal', + 'not_equal', + 'less', + 'less_or_equal', + 'greater', + 'greater_or_equal', + 'between', + 'is_null', + 'is_not_null', + ]; + + let $queryBuilder = $('#query_builder').queryBuilder({ + plugins: { + 'filter-description': { + icon: 'fa fa-info-circle', + mode: 'bootbox', + }, + 'bt-tooltip-errors': null, + }, + allow_empty: true, + allow_groups: false, + filters: [ + { + id: 'request_method', + label: I18n.t('admin.stats.fields.request_method.label'), + description: I18n.t('admin.stats.fields.request_method.description_markdown'), + type: 'string', + operators: selectOperators, + input: 'select', + values: { + 'get': 'GET', + 'post': 'POST', + 'put': 'PUT', + 'delete': 'DELETE', + 'head': 'HEAD', + 'patch': 'PATCH', + 'options': 'OPTIONS', + }, + }, + { + id: 'request_scheme', + label: I18n.t('admin.stats.fields.request_scheme.label'), + description: I18n.t('admin.stats.fields.request_scheme.description_markdown'), + type: 'string', + operators: selectOperators, + input: 'select', + values: { + 'http': 'http', + 'https': 'https', + }, + }, + { + id: 'request_host', + label: I18n.t('admin.stats.fields.request_host.label'), + description: I18n.t('admin.stats.fields.request_host.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_path', + label: I18n.t('admin.stats.fields.request_path.label'), + description: I18n.t('admin.stats.fields.request_path.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_url', + label: I18n.t('admin.stats.fields.request_url.label'), + description: I18n.t('admin.stats.fields.request_url.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip', + label: I18n.t('admin.stats.fields.request_ip.label'), + description: I18n.t('admin.stats.fields.request_ip.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip_country', + label: I18n.t('admin.stats.fields.request_ip_country.label'), + description: I18n.t('admin.stats.fields.request_ip_country.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip_region', + label: I18n.t('admin.stats.fields.request_ip_region.label'), + description: I18n.t('admin.stats.fields.request_ip_region.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip_city', + label: I18n.t('admin.stats.fields.request_ip_city.label'), + description: I18n.t('admin.stats.fields.request_ip_city.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_user_agent', + label: I18n.t('admin.stats.fields.request_user_agent.label'), + description: I18n.t('admin.stats.fields.request_user_agent.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_user_agent_family', + label: I18n.t('admin.stats.fields.request_user_agent_family.label'), + description: I18n.t('admin.stats.fields.request_user_agent_family.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_user_agent_type', + label: I18n.t('admin.stats.fields.request_user_agent_type.label'), + description: I18n.t('admin.stats.fields.request_user_agent_type.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_referer', + label: I18n.t('admin.stats.fields.request_referer.label'), + description: I18n.t('admin.stats.fields.request_referer.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'request_origin', + label: I18n.t('admin.stats.fields.request_origin.label'), + description: I18n.t('admin.stats.fields.request_origin.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'api_key', + label: I18n.t('admin.stats.fields.api_key.label'), + description: I18n.t('admin.stats.fields.api_key.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'user_email', + label: I18n.t('admin.stats.fields.user_email.label'), + description: I18n.t('admin.stats.fields.user_email.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'user_id', + label: I18n.t('admin.stats.fields.user_id.label'), + description: I18n.t('admin.stats.fields.user_id.description_markdown'), + type: 'string', + operators: stringOperators, + }, + { + id: 'response_status', + label: I18n.t('admin.stats.fields.response_status.label'), + description: I18n.t('admin.stats.fields.response_status.description_markdown'), + type: 'integer', + operators: numberOperators, + }, + { + id: 'gatekeeper_denied_code', + label: I18n.t('admin.stats.fields.gatekeeper_denied_code.label'), + description: I18n.t('admin.stats.fields.gatekeeper_denied_code.description_markdown'), + type: 'string', + operators: selectOperators, + input: 'select', + values: { + 'not_found': 'not_found', + 'api_key_missing': 'api_key_missing', + 'api_key_invalid': 'api_key_invalid', + 'api_key_disabled': 'api_key_disabled', + 'api_key_unverified': 'api_key_unverified', + 'api_key_unauthorized': 'api_key_unauthorized', + 'over_rate_limit': 'over_rate_limit', + 'internal_server_error': 'internal_server_error', + 'https_required': 'https_required', + }, + }, + { + id: 'response_time', + label: I18n.t('admin.stats.fields.response_time.label'), + description: I18n.t('admin.stats.fields.response_time.description_markdown'), + type: 'integer', + operators: numberOperators, + }, + { + id: 'response_content_type', + label: I18n.t('admin.stats.fields.response_content_type.label'), + description: I18n.t('admin.stats.fields.response_content_type.description_markdown'), + type: 'string', + operators: stringOperators, + }, + ], + }); + + let query = this.get('query'); + let rules; + if(query) { + rules = JSON.parse(query); + } + + if(rules) { + if(rules.condition) { + $queryBuilder.queryBuilder('setRules', rules); + } + + this.send('toggleFilters'); + this.send('toggleFilterType', 'builder'); + } else if(this.get('search')) { + this.send('toggleFilters'); + this.send('toggleFilterType', 'advanced'); + } + }, + + updateQueryBuilderRules: function() { + let query = this.get('query'); + let rules; + if(query) { + rules = JSON.parse(query); + } + + if(rules && rules.condition) { + $('#query_builder').queryBuilder('setRules', rules); + } else { + $('#query_builder').queryBuilder('reset'); + } + }.observes('query'), + + updateDateRange: function() { + let start = moment(this.get('start_at')); + let end = moment(this.get('end_at')); + + $('#reportrange span.text').html(start.format('MMM D, YYYY') + ' - ' + end.format('MMM D, YYYY')); + }.observes('start_at', 'end_at'), + + handleDateRangeChange(start, end) { + this.setProperties({ + 'start_at': start.format('YYYY-MM-DD'), + 'end_at': end.format('YYYY-MM-DD'), + }); + }, + + actions: { + toggleFilters() { + let $container = $('#filters_ui'); + let $icon = $('#filter_toggle .fa'); + if($container.is(':visible')) { + $icon.addClass('fa-caret-right'); + $icon.removeClass('fa-caret-down'); + } else { + $icon.addClass('fa-caret-down'); + $icon.removeClass('fa-caret-right'); + } + + $container.slideToggle(100); + }, + + toggleFilterType(type) { + $('.filter-type').hide(); + $('#filter_type_' + type).show(); + }, + + clickInterval(interval) { + this.set('interval', interval); + }, + + submit() { + if($('#filter_type_advanced').css('display') === 'none') { + this.set('search', ''); + this.set('query', JSON.stringify($('#query_builder').queryBuilder('getRules'))); + } else { + this.set('query', ''); + this.set('search', $('#filter_form input[name=search]').val()); + } + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/stats/users/results-table.js b/src/api-umbrella/admin-ui/app/components/stats/users/results-table.js new file mode 100644 index 000000000..1fabbcbfb --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/stats/users/results-table.js @@ -0,0 +1,88 @@ +import Ember from 'ember'; +import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; + +export default Ember.Component.extend({ + didInsertElement() { + this.$().find('table').DataTable({ + searching: false, + serverSide: true, + ajax: { + url: '/admin/stats/users.json', + data: function(data) { + return _.extend({}, data, this.get('allQueryParamValues')); + }.bind(this), + }, + order: [[4, 'desc']], + columns: [ + { + data: 'email', + title: 'Email', + defaultContent: '-', + render: function(email, type, data) { + if(type === 'display' && email && email !== '-') { + let params = _.clone(this.get('queryParamValues')); + params.search = 'user_id:"' + data.id + '"'; + let link = '#/stats/logs?' + $.param(params); + + return '' + _.escape(email) + ''; + } + + return email; + }.bind(this), + }, + { + data: 'first_name', + title: 'First Name', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'last_name', + title: 'Last Name', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + { + data: 'created_at', + type: 'date', + title: 'Signed Up', + defaultContent: '-', + render: DataTablesHelpers.renderTime, + }, + { + data: 'hits', + title: 'Hits', + defaultContent: '-', + render(number, type) { + if(type === 'display' && number && number !== '-') { + return numeral(number).format('0,0'); + } + + return number; + }, + }, + { + data: 'last_request_at', + type: 'date', + title: 'Last Request', + defaultContent: '-', + render: DataTablesHelpers.renderTime, + }, + { + data: 'use_description', + title: 'Use Description', + defaultContent: '-', + render: DataTablesHelpers.renderEscaped, + }, + ], + }); + }, + + refreshData: Ember.observer('allQueryParamValues', function() { + this.$().find('table').DataTable().draw(); + }), + + downloadUrl: Ember.computed('allQueryParamValues', function() { + return '/admin/stats/users.csv?' + $.param(this.get('allQueryParamValues')); + }), +}); diff --git a/src/api-umbrella/admin-ui/app/components/website-backends/index-table.js b/src/api-umbrella/admin-ui/app/components/website-backends/index-table.js new file mode 100644 index 000000000..88fced467 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/website-backends/index-table.js @@ -0,0 +1,30 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + didInsertElement() { + this.set('table', this.$().find('table').DataTable({ + serverSide: true, + ajax: '/api-umbrella/v1/website_backends.json', + pageLength: 50, + rowCallback(row, data) { + $(row).data('id', data.id); + }, + order: [[0, 'asc']], + columns: [ + { + data: 'frontend_host', + title: 'Host', + defaultContent: '-', + render: _.bind(function(name, type, data) { + if(type === 'display' && name && name !== '-') { + let link = '#/website_backends/' + data.id + '/edit'; + return '' + _.escape(name) + ''; + } + + return name; + }, this), + }, + ], + })); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/components/website-backends/record-form.js b/src/api-umbrella/admin-ui/app/components/website-backends/record-form.js new file mode 100644 index 000000000..4993fb25e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/website-backends/record-form.js @@ -0,0 +1,40 @@ +import Ember from 'ember'; +import Save from 'api-umbrella-admin-ui/mixins/save'; + +export default Ember.Component.extend(Save, { + backendProtocolOptions: [ + { id: 'http', name: 'http' }, + { id: 'https', name: 'https' }, + ], + + changeDefaultPort: function() { + let protocol = this.get('model.backendProtocol'); + let port = parseInt(this.get('model.serverPort'), 10); + if(protocol === 'https') { + if(!port || port === 80) { + this.set('model.serverPort', 443); + } + } else { + if(!port || port === 443) { + this.set('model.serverPort', 80); + } + } + }.observes('model.backendProtocol'), + + actions: { + submit() { + this.saveRecord({ + transitionToRoute: 'website_backends', + message: 'Successfully saved the "' + _.escape(this.get('model.frontendHost')) + '" website backend
Note: Your changes are not yet live. Publish Changes to send your updates live.', + }); + }, + + delete() { + this.destroyRecord({ + prompt: 'Are you sure you want to delete the website backend "' + _.escape(this.get('model.frontendHost')) + '"?', + transitionToRoute: 'website_backends', + message: 'Successfully deleted the "' + _.escape(this.get('model.frontendHost')) + '" website backend
Note: Your changes are not yet live. Publish Changes to send your updates live.', + }); + }, + }, +}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/.gitkeep b/src/api-umbrella/admin-ui/app/controllers/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/.gitkeep rename to src/api-umbrella/admin-ui/app/controllers/.gitkeep diff --git a/src/api-umbrella/admin-ui/app/controllers/application.js b/src/api-umbrella/admin-ui/app/controllers/application.js new file mode 100644 index 000000000..2296d9363 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/controllers/application.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + session: Ember.inject.service('session'), + + isLoading: null, + + actions: { + logout() { + this.get('session').invalidate(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/controllers/stats/base.js b/src/api-umbrella/admin-ui/app/controllers/stats/base.js new file mode 100644 index 000000000..6cea91e9a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/controllers/stats/base.js @@ -0,0 +1,23 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + tz: jstz.determine().name(), + search: '', + interval: 'day', + prefix: '0/', + region: 'world', + start_at: moment().subtract(29, 'days').format('YYYY-MM-DD'), + end_at: moment().format('YYYY-MM-DD'), + query: JSON.stringify({ + condition: 'AND', + rules: [{ + field: 'gatekeeper_denied_code', + id: 'gatekeeper_denied_code', + input: 'select', + operator: 'is_null', + type: 'string', + value: null, + }], + }), + beta_analytics: false, +}); diff --git a/src/api-umbrella/admin-ui/app/controllers/stats/drilldown.js b/src/api-umbrella/admin-ui/app/controllers/stats/drilldown.js new file mode 100644 index 000000000..aa4a2e3bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/controllers/stats/drilldown.js @@ -0,0 +1,14 @@ +import Base from './base'; + +export default Base.extend({ + queryParams: [ + 'tz', + 'start_at', + 'end_at', + 'interval', + 'query', + 'search', + 'prefix', + 'beta_analytics', + ], +}); diff --git a/src/api-umbrella/admin-ui/app/controllers/stats/logs.js b/src/api-umbrella/admin-ui/app/controllers/stats/logs.js new file mode 100644 index 000000000..c8981d75b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/controllers/stats/logs.js @@ -0,0 +1,13 @@ +import Base from './base'; + +export default Base.extend({ + queryParams: [ + 'tz', + 'start_at', + 'end_at', + 'interval', + 'query', + 'search', + 'beta_analytics', + ], +}); diff --git a/src/api-umbrella/admin-ui/app/controllers/stats/map.js b/src/api-umbrella/admin-ui/app/controllers/stats/map.js new file mode 100644 index 000000000..ef5f28b4e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/controllers/stats/map.js @@ -0,0 +1,14 @@ +import Base from './base'; + +export default Base.extend({ + queryParams: [ + 'tz', + 'start_at', + 'end_at', + 'query', + 'search', + 'region', + 'beta_analytics', + ], + +}); diff --git a/src/api-umbrella/admin-ui/app/controllers/stats/users.js b/src/api-umbrella/admin-ui/app/controllers/stats/users.js new file mode 100644 index 000000000..e4387a600 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/controllers/stats/users.js @@ -0,0 +1,12 @@ +import Base from './base'; + +export default Base.extend({ + queryParams: [ + 'tz', + 'start_at', + 'end_at', + 'query', + 'search', + 'beta_analytics', + ], +}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/helpers/.gitkeep b/src/api-umbrella/admin-ui/app/helpers/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/helpers/.gitkeep rename to src/api-umbrella/admin-ui/app/helpers/.gitkeep diff --git a/src/api-umbrella/admin-ui/app/helpers/format-date.js b/src/api-umbrella/admin-ui/app/helpers/format-date.js new file mode 100644 index 000000000..5a8713335 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/helpers/format-date.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; + +export function formatDate(params) { + let date = params[0]; + let format = params[1]; + + if(!format || !_.isString(format)) { + format = 'YYYY-MM-DD HH:mm Z'; + } + + if(date) { + return moment(date).format(format); + } else { + return ''; + } +} + +export default Ember.Helper.helper(formatDate); diff --git a/src/api-umbrella/admin-ui/app/helpers/guid-for.js b/src/api-umbrella/admin-ui/app/helpers/guid-for.js new file mode 100644 index 000000000..ddb5ad608 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/helpers/guid-for.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +export function guidFor(params) { + let object = params[0]; + + return Ember.guidFor(object); +} + +export default Ember.Helper.helper(guidFor); diff --git a/src/api-umbrella/admin-ui/app/helpers/html-safe.js b/src/api-umbrella/admin-ui/app/helpers/html-safe.js new file mode 100644 index 000000000..f87be49d7 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/helpers/html-safe.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; + +export function htmlSafe(params) { + let value = params[0]; + return new Ember.String.htmlSafe(value); +} + +export default Ember.Helper.helper(htmlSafe); diff --git a/src/api-umbrella/admin-ui/app/helpers/inflect.js b/src/api-umbrella/admin-ui/app/helpers/inflect.js new file mode 100644 index 000000000..0208c939c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/helpers/inflect.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +export function inflect(params) { + let word = params[0]; + let number = params[1]; + return inflection.inflect(word, number); +} + +export default Ember.Helper.helper(inflect); diff --git a/src/api-umbrella/admin-ui/app/helpers/t.js b/src/api-umbrella/admin-ui/app/helpers/t.js new file mode 100644 index 000000000..ef8c72035 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/helpers/t.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; + +export function t(params, options) { + let key = params[0]; + return I18n.t(key, options); +} + +export default Ember.Helper.helper(t); diff --git a/src/api-umbrella/admin-ui/app/index.html b/src/api-umbrella/admin-ui/app/index.html new file mode 100644 index 000000000..bafbc3f09 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/index.html @@ -0,0 +1,53 @@ + + + + + + ApiUmbrellaAdmin + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + + + + + {{content-for "body"}} + + + + + + + + + + {{content-for "body-footer"}} + + diff --git a/src/api-umbrella/admin-ui/app/initializers/bootbox.js b/src/api-umbrella/admin-ui/app/initializers/bootbox.js new file mode 100644 index 000000000..92647ac41 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/initializers/bootbox.js @@ -0,0 +1,10 @@ +export function initialize() { + bootbox.setDefaults({ + animate: false, + }); +} + +export default { + name: 'bootbox', + initialize, +}; diff --git a/src/api-umbrella/admin-ui/app/initializers/inflections.js b/src/api-umbrella/admin-ui/app/initializers/inflections.js new file mode 100644 index 000000000..e03358eea --- /dev/null +++ b/src/api-umbrella/admin-ui/app/initializers/inflections.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export function initialize() { + // So the Api model doesn't try to singularize the subSettings hasMany + // relationship (which leads to it trying to find the non-existent + // "api/sub-setting" model). + Ember.Inflector.inflector.irregular('sub-settings', 'sub-settings'); +} + +export default { + name: 'inflections', + initialize, +}; diff --git a/src/api-umbrella/admin-ui/app/initializers/pnotify.js b/src/api-umbrella/admin-ui/app/initializers/pnotify.js new file mode 100644 index 000000000..9a07923ae --- /dev/null +++ b/src/api-umbrella/admin-ui/app/initializers/pnotify.js @@ -0,0 +1,19 @@ +export function initialize() { + _.merge(PNotify.prototype.options, { + styling: 'bootstrap3', + width: '400px', + icon: false, + animation: 'none', + history: { + history: false, + }, + buttons: { + sticker: false, + }, + }); +} + +export default { + name: 'pnotify', + initialize, +}; diff --git a/src/api-umbrella/admin-ui/app/initializers/qtip.js b/src/api-umbrella/admin-ui/app/initializers/qtip.js new file mode 100644 index 000000000..e3d34562c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/initializers/qtip.js @@ -0,0 +1,67 @@ +export function initialize() { + $(document).on('click', 'a[rel=tooltip]', function(event) { + $(this).qtip({ + overwrite: false, + show: { + event: event.type, + ready: true, + solo: true, + }, + hide: { + event: 'unfocus', + }, + style: { + classes: 'qtip-bootstrap ' + $(this).data('tooltip-class'), + }, + position: { + viewport: true, + my: 'bottom left', + at: 'top center', + adjust: { + y: 2, + }, + }, + }, event); + + event.preventDefault(); + }); + + $(document).on('click', 'a[rel=popover]', function(event) { + $(this).qtip({ + overwrite: false, + show: { + event: event.type, + ready: true, + solo: true, + }, + hide: { + event: 'unfocus', + }, + content: { + text(event) { + let target = $(event.target).attr('href'); + let content = $(target).html(); + return content; + }, + }, + style: { + classes: 'qtip-bootstrap qtip-wide ' + $(this).data('tooltip-class'), + }, + position: { + viewport: false, + my: 'top left', + at: 'bottom center', + adjust: { + y: 2, + }, + }, + }, event); + + event.preventDefault(); + }); +} + +export default { + name: 'qtip', + initialize, +}; diff --git a/src/api-umbrella/admin-ui/app/instance-initializers/datatables.js b/src/api-umbrella/admin-ui/app/instance-initializers/datatables.js new file mode 100644 index 000000000..89763a818 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/instance-initializers/datatables.js @@ -0,0 +1,49 @@ +export function initialize(appInstance) { + // Defaults for DataTables. + _.merge($.fn.DataTable.defaults, { + // Don't show the DataTables processing message. We'll handle the processing + // message logic in preDrawCallback. + processing: false, + + // Enable global searching. + searching: true, + + // Re-arrange how the table and surrounding fields (pagination, search, etc) + // are laid out. + dom: 'rft<"row"<"col-sm-3 table-info"i><"col-sm-6 table-pagination"p><"col-sm-3 table-length"l>>', + + language: { + // Don't have an explicit label for the search field. Use a placeholder + // instead. + search: '', + searchPlaceholder: 'Search...', + }, + + preDrawCallback() { + if(!this.customProcessingCallbackSet) { + // Use a custom spinner to provide a more obvious processing message + // the overlays the entire table (this helps for long tables, where a + // simple processing message might appear out of your current view). + // This also standardizes the spinner with other loaders used + // throughout the Ember app. + // + // Set this early on during pre-draw so that the processing message shows + // up for the first load. + $(this).DataTable().on('processing', _.bind(function(event, settings, processing) { + if(processing) { + appInstance.lookup('service:busy').show(); + } else { + appInstance.lookup('service:busy').hide(); + } + }, this)); + + this.customProcessingCallbackSet = true; + } + }, + }); +} + +export default { + name: 'datatables', + initialize, +}; diff --git a/src/api-umbrella/admin-ui/app/instance-initializers/jquery-ajax.js b/src/api-umbrella/admin-ui/app/instance-initializers/jquery-ajax.js new file mode 100644 index 000000000..4157c9f3a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/instance-initializers/jquery-ajax.js @@ -0,0 +1,15 @@ +export function initialize(appInstance) { + let session = appInstance.lookup('service:session'); + $.ajaxPrefilter(function(options) { + session.authorize('authorizer:devise-server-side', function(apiKey, csrfToken) { + options.headers = options.headers || {}; + options.headers['X-Api-Key'] = apiKey; + options.headers['X-CSRF-Token'] = csrfToken; + }); + }); +} + +export default { + name: 'jquery-ajax', + initialize, +}; diff --git a/src/api-umbrella/admin-ui/app/mixins/confirmation.js b/src/api-umbrella/admin-ui/app/mixins/confirmation.js new file mode 100644 index 000000000..914541f1c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/mixins/confirmation.js @@ -0,0 +1,38 @@ +import Ember from 'ember'; +import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; + +export default Ember.Mixin.create(ConfirmationMixin, { + afterModel(model) { + // Store the full JSON representation of the model after fetching. This is + // used in isPageDirty() to determine if the model has changed. We can't + // rely on ember-data's builtin dirty tracking, since it considers all new + // records dirty and also doesn't currently support nested/embedded models: + // https://github.com/emberjs/rfcs/pull/21 + model.set('_confirmationRecordInitialSerialized', model.serialize()); + + // Determine when the record gets saved, since we don't want to prompt + // about navigating away if we're in the process of saving the record. + model.set('_confirmationRecordIsSaved', false); + model.on('didCreate', function() { + model.set('_confirmationRecordIsSaved', true); + }); + model.on('didUpdate', function() { + model.set('_confirmationRecordIsSaved', true); + }); + }, + + isPageDirty(model) { + if(model) { + let saved = model.get('_confirmationRecordIsSaved'); + if(saved) { + return false; + } else { + let initialSerialized = model.get('_confirmationRecordInitialSerialized'); + let currentSerialized = model.serialize(); + return !_.isEqual(currentSerialized, initialSerialized); + } + } else { + return false; + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/mixins/save.js b/src/api-umbrella/admin-ui/app/mixins/save.js new file mode 100644 index 000000000..f0eb9ec75 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/mixins/save.js @@ -0,0 +1,66 @@ +import Ember from 'ember'; + +export default Ember.Mixin.create({ + routing: Ember.inject.service('-routing'), + + scrollToErrors() { + $('#save_button').button('reset'); + $.scrollTo('#error_messages', { offset: -60, duration: 200 }); + }, + + saveRecord(options) { + let button = $('#save_button'); + button.button('loading'); + + this.setProperties({ + 'model.clientErrors': [], + 'model.serverErrors': [], + }); + + this.get('model').validate().then(function() { + if(this.get('model.validations.isValid') === false) { + this.set('model.clientErrors', this.get('model.validations.errors')); + this.scrollToErrors(); + } else { + this.get('model').save().then(function() { + button.button('reset'); + new PNotify({ + type: 'success', + title: 'Saved', + text: (_.isFunction(options.message)) ? options.message(this.get('model')) : options.message, + }); + + this.get('routing').transitionTo(options.transitionToRoute); + }.bind(this), function(error) { + // Set the errors from the server response on a "serverErrors" property + // for the error-messages component display. + if(error && error.errors) { + this.set('model.serverErrors', error.errors); + } else { + this.set('model.serverErrors', [{ message: 'Unexpected error' }]); + } + + this.scrollToErrors(); + }.bind(this)); + } + }.bind(this)); + }, + + destroyRecord(options) { + bootbox.confirm(options.prompt, function(result) { + if(result) { + this.get('model').destroyRecord().then(function() { + new PNotify({ + type: 'success', + title: 'Deleted', + text: (_.isFunction(options.message)) ? options.message(this.get('model')) : options.message, + }); + + this.get('routing').transitionTo(options.transitionToRoute); + }.bind(this), function(response) { + bootbox.alert('Unexpected error deleting record: ' + response.responseText); + }); + } + }.bind(this)); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/mixins/sortable.js b/src/api-umbrella/admin-ui/app/mixins/sortable.js new file mode 100644 index 000000000..a2b5fcbee --- /dev/null +++ b/src/api-umbrella/admin-ui/app/mixins/sortable.js @@ -0,0 +1,51 @@ +import Ember from 'ember'; + +export default Ember.Mixin.create({ + isReorderable: Ember.computed('sortableCollection.length', function() { + let length = this.get('sortableCollection.length'); + return (length && length > 1); + }), + + updateSortOrder(indexes) { + this.get('sortableCollection').forEach(function(record) { + let index = indexes[Ember.guidFor(record)]; + record.set('sortOrder', index); + }); + }, + + actions: { + reorderCollection(containerId) { + let $container = $('#' + containerId); + let $buttonText = $container.find('.reorder-button-text'); + + if($container.hasClass('reorder-active')) { + $buttonText.text($buttonText.data('originalText')); + } else { + $buttonText.data('originalText', $buttonText.text()); + $buttonText.text('Done'); + } + + $container.toggleClass('reorder-active'); + + let self = this; + $container.find('tbody').sortable({ + handle: '.reorder-handle', + placeholder: 'reorder-placeholder', + helper(event, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + }, + stop() { + let indexes = {}; + $(this).find('tr').each(function(index) { + indexes[$(this).data('guid')] = index; + }); + + self.updateSortOrder(indexes); + }, + }); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/mixins/uncached-model.js b/src/api-umbrella/admin-ui/app/mixins/uncached-model.js new file mode 100644 index 000000000..327f63a9a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/mixins/uncached-model.js @@ -0,0 +1,28 @@ +import Ember from 'ember'; + +export default Ember.Mixin.create({ + // Call before fetching a model to clear any client-side cache data. + // + // This works around a couple different issues with ember-data, which is why + // we're explicitly documenting this approach with a mixin. It's a bit + // heavy-handed by clearing the entire client-side cache, but since we + // generally want to re-fetch data from the server (in case other users are + // making edits at the same time), it's the simplest current approach. + // + // - Prevents duplicate embedded records from showing up after edits. Without + // this, if you add a new embedded record, save, and then visit the parent + // record again, the new embedded record will be duplicated twice. This is + // because we don't add client-side IDs for these embedded records so the + // ID-less embedded record and the one with the ID after saving will both + // be present. See https://github.com/emberjs/data/issues/1829 + // - Clears local edits if you make changes to a record, then navigate away + // (while explicitly confirming that you wanted to navigate away), and then + // come back to edit the same record. Other approaches to clear these + // changes don't seem to work as intended or as we want: + // - reload or shouldReloadRecord continues to persist the local edits + // despite fetching the record from the remote ajax call again. + // - rollbackAttributes doesn't work for embedded relationship data. + clearStoreCache() { + this.get('store').unloadAll(); + }, +}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/.gitkeep b/src/api-umbrella/admin-ui/app/models/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/models/.gitkeep rename to src/api-umbrella/admin-ui/app/models/.gitkeep diff --git a/src/api-umbrella/admin-ui/app/models/admin-group.js b/src/api-umbrella/admin-ui/app/models/admin-group.js new file mode 100644 index 000000000..6b77c818b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/admin-group.js @@ -0,0 +1,21 @@ +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + name: validator('presence', true), +}); + +export default DS.Model.extend(Validations, { + name: DS.attr(), + apiScopeIds: DS.attr({ defaultValue() { return [] } }), + permissionIds: DS.attr({ defaultValue() { return [] } }), + admins: DS.attr(), + createdAt: DS.attr(), + updatedAt: DS.attr(), + creator: DS.attr(), + updater: DS.attr(), +}).reopenClass({ + urlRoot: '/api-umbrella/v1/admin_groups', + singlePayloadKey: 'admin_group', + arrayPayloadKey: 'data', +}); diff --git a/src/api-umbrella/admin-ui/app/models/admin-permission.js b/src/api-umbrella/admin-ui/app/models/admin-permission.js new file mode 100644 index 000000000..993a10e35 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/admin-permission.js @@ -0,0 +1,9 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + name: DS.attr(), +}).reopenClass({ + urlRoot: '/api-umbrella/v1/admin_permissions', + singlePayloadKey: 'admin_permission', + arrayPayloadKey: 'admin_permissions', +}); diff --git a/src/api-umbrella/admin-ui/app/models/admin.js b/src/api-umbrella/admin-ui/app/models/admin.js new file mode 100644 index 000000000..622ce69a5 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/admin.js @@ -0,0 +1,27 @@ +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + username: validator('presence', true), +}); + +export default DS.Model.extend(Validations, { + username: DS.attr(), + email: DS.attr(), + name: DS.attr(), + superuser: DS.attr(), + groupIds: DS.attr({ defaultValue() { return [] } }), + signInCount: DS.attr(), + lastSignInAt: DS.attr(), + lastSignInIp: DS.attr(), + lastSignInProvider: DS.attr(), + authenticationToken: DS.attr(), + createdAt: DS.attr(), + updatedAt: DS.attr(), + creator: DS.attr(), + updater: DS.attr(), +}).reopenClass({ + urlRoot: '/api-umbrella/v1/admins', + singlePayloadKey: 'admin', + arrayPayloadKey: 'data', +}); diff --git a/src/api-umbrella/admin-ui/app/models/api-scope.js b/src/api-umbrella/admin-ui/app/models/api-scope.js new file mode 100644 index 000000000..a6326aae7 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api-scope.js @@ -0,0 +1,39 @@ +import Ember from 'ember'; +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + name: validator('presence', true), + host: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.host_format_with_wildcard, + message: I18n.t('errors.messages.invalid_host_format'), + }), + ], + pathPrefix: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.url_prefix_format, + message: I18n.t('errors.messages.invalid_url_prefix_format'), + }), + ], +}); + +export default DS.Model.extend(Validations, { + name: DS.attr(), + host: DS.attr(), + pathPrefix: DS.attr(), + createdAt: DS.attr(), + updatedAt: DS.attr(), + creator: DS.attr(), + updater: DS.attr(), + + displayName: Ember.computed('name', 'host', 'pathPrefix', function() { + return this.get('name') + ' - ' + this.get('host') + this.get('pathPrefix'); + }), +}).reopenClass({ + urlRoot: '/api-umbrella/v1/api_scopes', + singlePayloadKey: 'api_scope', + arrayPayloadKey: 'data', +}); diff --git a/src/api-umbrella/admin-ui/app/models/api-user-role.js b/src/api-umbrella/admin-ui/app/models/api-user-role.js new file mode 100644 index 000000000..146433959 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api-user-role.js @@ -0,0 +1,7 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ +}).reopenClass({ + urlRoot: '/api-umbrella/v1/user_roles', + arrayPayloadKey: 'user_roles', +}); diff --git a/src/api-umbrella/admin-ui/app/models/api-user.js b/src/api-umbrella/admin-ui/app/models/api-user.js new file mode 100644 index 000000000..3c3a3dc77 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api-user.js @@ -0,0 +1,79 @@ +import Ember from 'ember'; +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + firstName: validator('presence', true), + lastName: validator('presence', true), + email: validator('presence', true), +}); + +export default DS.Model.extend(Validations, { + apiKey: DS.attr(), + apiKeyHidesAt: DS.attr(), + apiKeyPreview: DS.attr(), + firstName: DS.attr(), + lastName: DS.attr(), + email: DS.attr(), + emailVerified: DS.attr(), + website: DS.attr(), + useDescription: DS.attr(), + registrationSource: DS.attr(), + termsAndConditions: DS.attr(), + sendWelcomeEmail: DS.attr(), + throttleByIp: DS.attr('boolean'), + roles: DS.attr(), + enabled: DS.attr('boolean'), + createdAt: DS.attr(), + updatedAt: DS.attr(), + creator: DS.attr(), + updater: DS.attr(), + registrationIp: DS.attr(), + registrationUserAgent: DS.attr(), + registrationReferer: DS.attr(), + registrationOrigin: DS.attr(), + + settings: DS.belongsTo('api/settings', { async: false }), + + ready() { + this.setDefaults(); + this._super(); + }, + + setDefaults() { + if(this.get('throttleByIp') === undefined) { + this.set('throttleByIp', false); + } + + if(this.get('enabled') === undefined) { + this.set('enabled', true); + } + + if(!this.get('settings')) { + this.set('settings', this.get('store').createRecord('api/settings')); + } + + if(!this.get('registrationSource') && this.get('isNew')) { + this.set('registrationSource', 'web_admin'); + } + }, + + rolesString: Ember.computed('roles', { + get() { + let rolesString = ''; + if(this.get('roles')) { + rolesString = this.get('roles').join(','); + } + return rolesString; + }, + set(key, value) { + let roles = value.split(','); + this.set('roles', roles); + return value; + }, + }), +}).reopenClass({ + urlRoot: '/api-umbrella/v1/users', + singlePayloadKey: 'user', + arrayPayloadKey: 'data', +}); diff --git a/src/api-umbrella/admin-ui/app/models/api.js b/src/api-umbrella/admin-ui/app/models/api.js new file mode 100644 index 000000000..abfef585f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api.js @@ -0,0 +1,71 @@ +import Ember from 'ember'; +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + name: validator('presence', true), + frontendHost: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.host_format_with_wildcard, + message: I18n.t('errors.messages.invalid_host_format'), + }), + ], + backendHost: [ + validator('presence', { + presence: true, + disabled: Ember.computed('model.frontendHost', function() { + return (this.get('model.frontendHost') && this.get('model.frontendHost')[0] === '*'); + }), + }), + validator('format', { + regex: CommonValidations.host_format_with_wildcard, + message: I18n.t('errors.messages.invalid_host_format'), + disabled: Ember.computed('model.backendHost', function() { + return !this.get('model.backendHost'); + }), + }), + ], +}); + +export default DS.Model.extend(Validations, { + name: DS.attr(), + sortOrder: DS.attr('number'), + backendProtocol: DS.attr('string', { defaultValue: 'http' }), + frontendHost: DS.attr(), + backendHost: DS.attr(), + balanceAlgorithm: DS.attr('string', { defaultValue: 'least_conn' }), + createdAt: DS.attr(), + updatedAt: DS.attr(), + creator: DS.attr(), + updater: DS.attr(), + + servers: DS.hasMany('api/server', { async: false }), + urlMatches: DS.hasMany('api/url-match', { async: false }), + settings: DS.belongsTo('api/settings', { async: false }), + subSettings: DS.hasMany('api/sub-settings', { async: false }), + rewrites: DS.hasMany('api/rewrites', { async: false }), + + ready() { + this.setDefaults(); + this._super(); + }, + + setDefaults() { + if(!this.get('settings')) { + this.set('settings', this.get('store').createRecord('api/settings')); + } + }, + + exampleIncomingUrlRoot: Ember.computed('frontendHost', function() { + return 'http://' + (this.get('frontendHost') || ''); + }), + + exampleOutgoingUrlRoot: Ember.computed('backendHost', function() { + return 'http://' + (this.get('backendHost') || this.get('frontendHost') || ''); + }), +}).reopenClass({ + urlRoot: '/api-umbrella/v1/apis', + singlePayloadKey: 'api', + arrayPayloadKey: 'data', +}); diff --git a/src/api-umbrella/admin-ui/app/models/api/rate-limit.js b/src/api-umbrella/admin-ui/app/models/api/rate-limit.js new file mode 100644 index 000000000..9194ae3ab --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api/rate-limit.js @@ -0,0 +1,54 @@ +import Ember from 'ember'; +import DS from 'ember-data'; + +export default DS.Model.extend({ + duration: DS.attr('number'), + limitBy: DS.attr(), + limit: DS.attr(), + responseHeaders: DS.attr(), + + ready() { + this.setDefaults(); + this._super(); + }, + + setDefaults() { + let duration = this.get('duration'); + if(duration) { + let days = duration / 86400000; + let hours = duration / 3600000; + let minutes = duration / 60000; + let seconds = duration / 1000; + + if(days % 1 === 0) { + this.setProperties({ + durationInUnits: days, + durationUnits: 'days', + }); + } else if(hours % 1 === 0) { + this.setProperties({ + durationInUnits: hours, + durationUnits: 'hours', + }); + } else if(minutes % 1 === 0) { + this.setProperties({ + durationInUnits: minutes, + durationUnits: 'minutes', + }); + } else { + this.setProperties({ + durationInUnits: seconds, + durationUnits: 'seconds', + }); + } + } + }, + + durationInUnitsDidChange: Ember.observer('durationInUnits', 'durationUnits', function() { + if(this.get('durationUnits')) { + let inUnits = parseInt(this.get('durationInUnits'), 10); + let units = this.get('durationUnits'); + this.set('duration', moment.duration(inUnits, units).asMilliseconds()); + } + }), +}); diff --git a/src/api-umbrella/admin-ui/app/models/api/rewrite.js b/src/api-umbrella/admin-ui/app/models/api/rewrite.js new file mode 100644 index 000000000..f372bdec4 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api/rewrite.js @@ -0,0 +1,9 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + sortOrder: DS.attr('number'), + matcherType: DS.attr(), + httpMethod: DS.attr(), + frontendMatcher: DS.attr(), + backendReplacement: DS.attr(), +}); diff --git a/src/api-umbrella/admin-ui/app/models/api/server.js b/src/api-umbrella/admin-ui/app/models/api/server.js new file mode 100644 index 000000000..2eebd79a3 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api/server.js @@ -0,0 +1,28 @@ +import Ember from 'ember'; +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + host: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.host_format, + message: I18n.t('errors.messages.invalid_host_format'), + }), + ], + port: [ + validator('presence', true), + validator('number', { allowString: true }), + ], +}); + +export default DS.Model.extend(Validations, { + host: DS.attr(), + port: DS.attr('number'), + + hostWithPort: Ember.computed('host', 'port', function() { + return _.compact([this.get('host'), this.get('port')]).join(':'); + }), +}).reopenClass({ + validationClass: Validations, +}); diff --git a/src/api-umbrella/admin-ui/app/models/api/settings.js b/src/api-umbrella/admin-ui/app/models/api/settings.js new file mode 100644 index 000000000..c0ac58f19 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api/settings.js @@ -0,0 +1,116 @@ +import Ember from 'ember'; +import DS from 'ember-data'; + +export default DS.Model.extend({ + appendQueryString: DS.attr(), + headersString: DS.attr(), + httpBasicAuth: DS.attr(), + requireHttps: DS.attr(), + disableApiKey: DS.attr(), + apiKeyVerificationLevel: DS.attr(), + requiredRoles: DS.attr(), + requiredRolesOverride: DS.attr(), + allowedIps: DS.attr(), + allowedReferers: DS.attr(), + rateLimitMode: DS.attr(), + anonymousRateLimitBehavior: DS.attr(), + authenticatedRateLimitBehavior: DS.attr(), + passApiKeyHeader: DS.attr(), + passApiKeyQueryParam: DS.attr(), + defaultResponseHeadersString: DS.attr(), + overrideResponseHeadersString: DS.attr(), + errorTemplates: DS.attr(), + errorDataYamlStrings: DS.attr(), + + rateLimits: DS.hasMany('api/rate-limit', { async: false }), + + ready() { + this.setDefaults(); + this._super(); + }, + + setDefaults() { + if(this.get('rateLimitMode') === undefined) { + this.set('rateLimitMode', null); + } + + // Make sure at least an empty object exists so the form builder can dive + // into this section even when there's no pre-existing data. + if(!this.get('errorTemplates')) { + this.set('errorTemplates', Ember.Object.create({})); + } + + if(!this.get('errorDataYamlStrings')) { + this.set('errorDataYamlStrings', Ember.Object.create({})); + } + }, + + requiredRolesString: Ember.computed('requiredRoles', { + get() { + let rolesString = ''; + if(this.get('requiredRoles')) { + rolesString = this.get('requiredRoles').join(','); + } + return rolesString; + }, + set(key, value) { + let roles = _.compact(value.split(',')); + if(roles.length === 0) { roles = null; } + this.set('requiredRoles', roles); + return value; + }, + }), + + allowedIpsString: Ember.computed('allowedIps', { + get() { + let allowedIpsString = ''; + if(this.get('allowedIps')) { + allowedIpsString = this.get('allowedIps').join('\n'); + } + return allowedIpsString; + }, + set(key, value) { + let ips = _.compact(value.split(/[\r\n]+/)); + if(ips.length === 0) { ips = null; } + this.set('allowedIps', ips); + return value; + }, + }), + + allowedReferersString: Ember.computed('allowedReferers', { + get() { + let allowedReferersString = ''; + if(this.get('allowedReferers')) { + allowedReferersString = this.get('allowedReferers').join('\n'); + } + return allowedReferersString; + }, + set(key, value) { + let referers = _.compact(value.split(/[\r\n]+/)); + if(referers.length === 0) { referers = null; } + this.set('allowedReferers', referers); + return value; + }, + }), + + passApiKey: Ember.computed('passApiKeyHeader', 'passApiKeyQueryParam', function() { + let options = Ember.A([]); + if(this.get('passApiKeyHeader')) { + options.pushObject('header'); + } + if(this.get('passApiKeyQueryParam')) { + options.pushObject('param'); + } + return options; + }), + + passApiKeyDidChange: Ember.observer('passApiKey.@each', function() { + let options = this.get('passApiKey'); + this.set('passApiKeyHeader', options.includes('header')); + this.set('passApiKeyQueryParam', options.includes('param')); + }), + + isRateLimitModeCustom: Ember.computed('rateLimitMode', function() { + return (this.get('rateLimitMode') === 'custom'); + }), +}); diff --git a/src/api-umbrella/admin-ui/app/models/api/sub-settings.js b/src/api-umbrella/admin-ui/app/models/api/sub-settings.js new file mode 100644 index 000000000..1903c6ddc --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api/sub-settings.js @@ -0,0 +1,20 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + sortOrder: DS.attr('number'), + httpMethod: DS.attr(), + regex: DS.attr(), + + settings: DS.belongsTo('api/settings', { async: false }), + + ready() { + this.setDefaults(); + this._super(); + }, + + setDefaults() { + if(!this.get('settings')) { + this.set('settings', this.get('store').createRecord('api/settings')); + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/models/api/url-match.js b/src/api-umbrella/admin-ui/app/models/api/url-match.js new file mode 100644 index 000000000..564734bd2 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/api/url-match.js @@ -0,0 +1,32 @@ +import Ember from 'ember'; +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + frontendPrefix: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.url_prefix_format, + message: I18n.t('errors.messages.invalid_url_prefix_format'), + }), + ], + backendPrefix: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.url_prefix_format, + message: I18n.t('errors.messages.invalid_url_prefix_format'), + }), + ], +}); + +export default DS.Model.extend(Validations, { + sortOrder: DS.attr('number'), + frontendPrefix: DS.attr(), + backendPrefix: DS.attr(), + + backendPrefixWithDefault: Ember.computed('backendPrefix', 'frontendPrefix', function() { + return this.get('backendPrefix') || this.get('frontendPrefix'); + }), +}).reopenClass({ + validationClass: Validations, +}); diff --git a/src/api-umbrella/admin-ui/app/models/config-pending-changes.js b/src/api-umbrella/admin-ui/app/models/config-pending-changes.js new file mode 100644 index 000000000..0379d2b48 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/config-pending-changes.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +let ConfigPendingChanges = Ember.Object.extend(Ember.Evented, { + config: null, +}); + +ConfigPendingChanges.reopenClass({ + urlRoot: '/api-umbrella/v1/config/pending_changes.json', + + fetch(params) { + return new Ember.RSVP.Promise(function(resolve, reject) { + return $.ajax({ + url: this.urlRoot, + data: params, + }).then(function(data) { + resolve(new ConfigPendingChanges(data)); + }, function() { + reject(); + }); + }.bind(this)); + }, +}); + +export default ConfigPendingChanges; diff --git a/src/api-umbrella/admin-ui/app/models/stats/drilldown.js b/src/api-umbrella/admin-ui/app/models/stats/drilldown.js new file mode 100644 index 000000000..e47c0837e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/stats/drilldown.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +let Drilldown = Ember.Object.extend(Ember.Evented, { + results: null, +}); + +Drilldown.reopenClass({ + urlRoot: '/api-umbrella/v1/analytics/drilldown.json', + + find(params) { + return new Ember.RSVP.Promise(function(resolve, reject) { + return $.ajax({ + url: this.urlRoot, + data: params, + }).then(function(data) { + resolve(new Drilldown(data)); + }, function() { + reject(); + }); + }.bind(this)); + }, +}); + +export default Drilldown; diff --git a/src/api-umbrella/admin-ui/app/models/stats/logs.js b/src/api-umbrella/admin-ui/app/models/stats/logs.js new file mode 100644 index 000000000..238f9f2bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/stats/logs.js @@ -0,0 +1,27 @@ +import Ember from 'ember'; + +let Logs = Ember.Object.extend(Ember.Evented, { + hits_over_time: null, + stats: null, + facets: null, + logs: null, +}); + +Logs.reopenClass({ + urlRoot: '/admin/stats/search.json', + + find(params) { + return new Ember.RSVP.Promise(function(resolve, reject) { + return $.ajax({ + url: this.urlRoot, + data: params, + }).then(function(data) { + resolve(new Logs(data)); + }, function() { + reject(); + }); + }.bind(this)); + }, +}); + +export default Logs; diff --git a/src/api-umbrella/admin-ui/app/models/stats/map.js b/src/api-umbrella/admin-ui/app/models/stats/map.js new file mode 100644 index 000000000..a8be8a956 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/stats/map.js @@ -0,0 +1,27 @@ +import Ember from 'ember'; + +let Map = Ember.Object.extend(Ember.Evented, { + hits_over_time: null, + stats: null, + facets: null, + logs: null, +}); + +Map.reopenClass({ + urlRoot: '/admin/stats/map.json', + + find(params) { + return new Ember.RSVP.Promise(function(resolve, reject) { + return $.ajax({ + url: this.urlRoot, + data: params, + }).then(function(data) { + resolve(new Map(data)); + }, function() { + reject(); + }); + }.bind(this)); + }, +}); + +export default Map; diff --git a/src/api-umbrella/admin-ui/app/models/website-backend.js b/src/api-umbrella/admin-ui/app/models/website-backend.js new file mode 100644 index 000000000..c3058446b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/models/website-backend.js @@ -0,0 +1,39 @@ +import DS from 'ember-data'; +import { validator, buildValidations } from 'ember-cp-validations'; + +const Validations = buildValidations({ + frontendHost: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.host_format_with_wildcard, + message: I18n.t('errors.messages.invalid_host_format'), + }), + ], + backendProtocol: validator('presence', true), + serverHost: [ + validator('presence', true), + validator('format', { + regex: CommonValidations.host_format_with_wildcard, + message: I18n.t('errors.messages.invalid_host_format'), + }), + ], + serverPort: [ + validator('presence', true), + validator('number', { allowString: true }), + ], +}); + +export default DS.Model.extend(Validations, { + frontendHost: DS.attr(), + backendProtocol: DS.attr('string', { defaultValue: 'http' }), + serverHost: DS.attr(), + serverPort: DS.attr('number', { defaultValue: 80 }), + createdAt: DS.attr(), + updatedAt: DS.attr(), + creator: DS.attr(), + updater: DS.attr(), +}).reopenClass({ + urlRoot: '/api-umbrella/v1/website_backends', + singlePayloadKey: 'website_backend', + arrayPayloadKey: 'data', +}); diff --git a/src/api-umbrella/admin-ui/app/resolver.js b/src/api-umbrella/admin-ui/app/resolver.js new file mode 100644 index 000000000..2fb563d6c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/resolver.js @@ -0,0 +1,3 @@ +import Resolver from 'ember-resolver'; + +export default Resolver; diff --git a/src/api-umbrella/admin-ui/app/router.js b/src/api-umbrella/admin-ui/app/router.js new file mode 100644 index 000000000..a59127e58 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/router.js @@ -0,0 +1,58 @@ +import Ember from 'ember'; +import config from './config/environment'; + +const Router = Ember.Router.extend({ + location: config.locationType, +}); + +Router.map(function() { + this.route('apis', { path: '/apis' }, function() { + this.route('new'); + this.route('edit', { path: '/:apiId/edit' }); + }); + + this.route('api_users', { path: '/api_users' }, function() { + this.route('new'); + this.route('edit', { path: '/:apiUserId/edit' }); + }); + + this.route('admins', { path: '/admins' }, function() { + this.route('new'); + this.route('edit', { path: '/:adminId/edit' }); + }); + + this.route('api_scopes', { path: '/api_scopes' }, function() { + this.route('new'); + this.route('edit', { path: '/:apiScopeId/edit' }); + }); + + this.route('admin_groups', { path: '/admin_groups' }, function() { + this.route('new'); + this.route('edit', { path: '/:adminGroupId/edit' }); + }); + + this.route('config', { path: '/config' }, function() { + this.route('publish'); + }); + + this.route('stats', { path: '/stats' }, function() { + this.route('drilldown', { path: '/drilldown' }); + this.route('drilldown-legacy', { path: '/drilldown/*legacyParams' }); + this.route('logs', { path: '/logs' }); + this.route('logs-legacy', { path: '/logs/*legacyParams' }); + this.route('users', { path: '/users' }); + this.route('users-legacy', { path: '/users/*legacyParams' }); + this.route('map', { path: '/map' }); + this.route('map-legacy', { path: '/map/*legacyParams' }); + }); + + this.route('website_backends', { path: '/website_backends' }, function() { + this.route('new'); + this.route('edit', { path: '/:websiteBackendId/edit' }); + }); + + this.route('login'); + this.route('not-found', { path: '/*wildcard' }); +}); + +export default Router; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/.gitkeep b/src/api-umbrella/admin-ui/app/routes/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/routes/.gitkeep rename to src/api-umbrella/admin-ui/app/routes/.gitkeep diff --git a/src/api-umbrella/admin-ui/app/routes/admin-groups/base.js b/src/api-umbrella/admin-ui/app/routes/admin-groups/base.js new file mode 100644 index 000000000..48853d09d --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admin-groups/base.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + setupController(controller, model) { + controller.set('model', model); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-users').addClass('active'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admin-groups/edit.js b/src/api-umbrella/admin-ui/app/routes/admin-groups/edit.js new file mode 100644 index 000000000..d1b2fe827 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admin-groups/edit.js @@ -0,0 +1,8 @@ +import Form from './form'; + +export default Form.extend({ + model(params) { + this.clearStoreCache(); + return this.get('store').findRecord('admin-group', params.adminGroupId); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admin-groups/form.js b/src/api-umbrella/admin-ui/app/routes/admin-groups/form.js new file mode 100644 index 000000000..3f6d7b7bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admin-groups/form.js @@ -0,0 +1,6 @@ +import Base from './base'; +import Confirmation from 'api-umbrella-admin-ui/mixins/confirmation'; +import UncachedModel from 'api-umbrella-admin-ui/mixins/uncached-model'; + +export default Base.extend(Confirmation, UncachedModel, { +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admin-groups/index.js b/src/api-umbrella/admin-ui/app/routes/admin-groups/index.js new file mode 100644 index 000000000..a57fe0956 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admin-groups/index.js @@ -0,0 +1,4 @@ +import Base from './base'; + +export default Base.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admin-groups/new.js b/src/api-umbrella/admin-ui/app/routes/admin-groups/new.js new file mode 100644 index 000000000..d88478b57 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admin-groups/new.js @@ -0,0 +1,7 @@ +import Form from './form'; + +export default Form.extend({ + model() { + return this.get('store').createRecord('admin-group'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admins/base.js b/src/api-umbrella/admin-ui/app/routes/admins/base.js new file mode 100644 index 000000000..48853d09d --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admins/base.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + setupController(controller, model) { + controller.set('model', model); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-users').addClass('active'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admins/edit.js b/src/api-umbrella/admin-ui/app/routes/admins/edit.js new file mode 100644 index 000000000..bdb075499 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admins/edit.js @@ -0,0 +1,8 @@ +import Form from './form'; + +export default Form.extend({ + model(params) { + this.clearStoreCache(); + return this.get('store').findRecord('admin', params.adminId); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admins/form.js b/src/api-umbrella/admin-ui/app/routes/admins/form.js new file mode 100644 index 000000000..3f6d7b7bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admins/form.js @@ -0,0 +1,6 @@ +import Base from './base'; +import Confirmation from 'api-umbrella-admin-ui/mixins/confirmation'; +import UncachedModel from 'api-umbrella-admin-ui/mixins/uncached-model'; + +export default Base.extend(Confirmation, UncachedModel, { +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admins/index.js b/src/api-umbrella/admin-ui/app/routes/admins/index.js new file mode 100644 index 000000000..a57fe0956 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admins/index.js @@ -0,0 +1,4 @@ +import Base from './base'; + +export default Base.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/routes/admins/new.js b/src/api-umbrella/admin-ui/app/routes/admins/new.js new file mode 100644 index 000000000..62023949c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/admins/new.js @@ -0,0 +1,7 @@ +import Form from './form'; + +export default Form.extend({ + model() { + return this.get('store').createRecord('admin'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-scopes/base.js b/src/api-umbrella/admin-ui/app/routes/api-scopes/base.js new file mode 100644 index 000000000..48853d09d --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-scopes/base.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + setupController(controller, model) { + controller.set('model', model); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-users').addClass('active'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-scopes/edit.js b/src/api-umbrella/admin-ui/app/routes/api-scopes/edit.js new file mode 100644 index 000000000..598a1255a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-scopes/edit.js @@ -0,0 +1,8 @@ +import Form from './form'; + +export default Form.extend({ + model(params) { + this.clearStoreCache(); + return this.get('store').findRecord('api-scope', params.apiScopeId); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-scopes/form.js b/src/api-umbrella/admin-ui/app/routes/api-scopes/form.js new file mode 100644 index 000000000..3f6d7b7bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-scopes/form.js @@ -0,0 +1,6 @@ +import Base from './base'; +import Confirmation from 'api-umbrella-admin-ui/mixins/confirmation'; +import UncachedModel from 'api-umbrella-admin-ui/mixins/uncached-model'; + +export default Base.extend(Confirmation, UncachedModel, { +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-scopes/index.js b/src/api-umbrella/admin-ui/app/routes/api-scopes/index.js new file mode 100644 index 000000000..a57fe0956 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-scopes/index.js @@ -0,0 +1,4 @@ +import Base from './base'; + +export default Base.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-scopes/new.js b/src/api-umbrella/admin-ui/app/routes/api-scopes/new.js new file mode 100644 index 000000000..2f5714abe --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-scopes/new.js @@ -0,0 +1,7 @@ +import Form from './form'; + +export default Form.extend({ + model() { + return this.get('store').createRecord('api-scope'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-users/base.js b/src/api-umbrella/admin-ui/app/routes/api-users/base.js new file mode 100644 index 000000000..48853d09d --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-users/base.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + setupController(controller, model) { + controller.set('model', model); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-users').addClass('active'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-users/edit.js b/src/api-umbrella/admin-ui/app/routes/api-users/edit.js new file mode 100644 index 000000000..f930b0af3 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-users/edit.js @@ -0,0 +1,8 @@ +import Form from './form'; + +export default Form.extend({ + model(params) { + this.clearStoreCache(); + return this.get('store').findRecord('api-user', params.apiUserId); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-users/form.js b/src/api-umbrella/admin-ui/app/routes/api-users/form.js new file mode 100644 index 000000000..3f6d7b7bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-users/form.js @@ -0,0 +1,6 @@ +import Base from './base'; +import Confirmation from 'api-umbrella-admin-ui/mixins/confirmation'; +import UncachedModel from 'api-umbrella-admin-ui/mixins/uncached-model'; + +export default Base.extend(Confirmation, UncachedModel, { +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-users/index.js b/src/api-umbrella/admin-ui/app/routes/api-users/index.js new file mode 100644 index 000000000..a57fe0956 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-users/index.js @@ -0,0 +1,4 @@ +import Base from './base'; + +export default Base.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/routes/api-users/new.js b/src/api-umbrella/admin-ui/app/routes/api-users/new.js new file mode 100644 index 000000000..ccf8e7185 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/api-users/new.js @@ -0,0 +1,7 @@ +import Form from './form'; + +export default Form.extend({ + model() { + return this.get('store').createRecord('api-user'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/apis/base.js b/src/api-umbrella/admin-ui/app/routes/apis/base.js new file mode 100644 index 000000000..4577dc439 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/apis/base.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + setupController(controller, model) { + controller.set('model', model); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-config').addClass('active'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/apis/edit.js b/src/api-umbrella/admin-ui/app/routes/apis/edit.js new file mode 100644 index 000000000..dbeb300de --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/apis/edit.js @@ -0,0 +1,8 @@ +import Form from './form'; + +export default Form.extend({ + model(params) { + this.clearStoreCache(); + return this.get('store').findRecord('api', params.apiId); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/apis/form.js b/src/api-umbrella/admin-ui/app/routes/apis/form.js new file mode 100644 index 000000000..3f6d7b7bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/apis/form.js @@ -0,0 +1,6 @@ +import Base from './base'; +import Confirmation from 'api-umbrella-admin-ui/mixins/confirmation'; +import UncachedModel from 'api-umbrella-admin-ui/mixins/uncached-model'; + +export default Base.extend(Confirmation, UncachedModel, { +}); diff --git a/src/api-umbrella/admin-ui/app/routes/apis/index.js b/src/api-umbrella/admin-ui/app/routes/apis/index.js new file mode 100644 index 000000000..a57fe0956 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/apis/index.js @@ -0,0 +1,4 @@ +import Base from './base'; + +export default Base.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/routes/apis/new.js b/src/api-umbrella/admin-ui/app/routes/apis/new.js new file mode 100644 index 000000000..35e9ae311 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/apis/new.js @@ -0,0 +1,9 @@ +import Form from './form'; + +export default Form.extend({ + model() { + return this.get('store').createRecord('api', { + frontendHost: location.hostname, + }); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/application.js b/src/api-umbrella/admin-ui/app/routes/application.js new file mode 100644 index 000000000..d77d115a3 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/application.js @@ -0,0 +1,51 @@ +import Ember from 'ember'; +import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin'; + +export default Ember.Route.extend(ApplicationRouteMixin, { + busy: Ember.inject.service('busy'), + + // By default, ember-simple-auth sets the "session.attemptedTransition" value + // to track where to redirect unauthenticated users to after logging in. + // However, since we're using a server-side login page, this variable + // disappears after the server-side login redirect. So instead, we'll store + // just the string value of the attempted transition and persist it in the + // session store so it's available after the server-side login. + attemptedTransitionChange: Ember.observer('session.attemptedTransition', function() { + const attemptedTransition = this.get('session.attemptedTransition'); + if(attemptedTransition) { + this.get('session').set('data.attemptedTransitionUrl', attemptedTransition.intent.url); + } else { + this.get('session').set('data.attemptedTransitionUrl', null); + } + }), + + // After successfully logging in, then redirect to the URL the user was + // originally trying to access. Since we're using a server-side login page, + // we need to do this a little differently than ember-simple-auth's default + // mechanism. We need to use the "attempedTransitionUrl" string value we + // persist in the session store. + sessionAuthenticated() { + const attemptedTransitionUrl = this.get('session.data.attemptedTransitionUrl'); + if(attemptedTransitionUrl) { + this.transitionTo(attemptedTransitionUrl); + this.set('session.attemptedTransition', null); + this.get('session').set('data.attemptedTransitionUrl', null); + } else { + this.transitionTo(this.get('routeAfterAuthentication')); + } + }, + + actions: { + loading(transition) { + let busy = this.get('busy'); + busy.show(); + transition.promise.finally(function() { + busy.hide(); + }); + }, + + error() { + this.get('busy').hide(); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/config/publish.js b/src/api-umbrella/admin-ui/app/routes/config/publish.js new file mode 100644 index 000000000..4576060a9 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/config/publish.js @@ -0,0 +1,16 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; +import ConfigPendingChanges from 'api-umbrella-admin-ui/models/config-pending-changes'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + model() { + return ConfigPendingChanges.fetch(); + }, + + setupController(controller, model) { + controller.set('model', model); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-config').addClass('active'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/index.js b/src/api-umbrella/admin-ui/app/routes/index.js new file mode 100644 index 000000000..30e4e301b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/index.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { +}); diff --git a/src/api-umbrella/admin-ui/app/routes/loading.js b/src/api-umbrella/admin-ui/app/routes/loading.js new file mode 100644 index 000000000..096e3c59c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/loading.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({}); diff --git a/src/api-umbrella/admin-ui/app/routes/login.js b/src/api-umbrella/admin-ui/app/routes/login.js new file mode 100644 index 000000000..b45aec26e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/login.js @@ -0,0 +1,16 @@ +import Ember from 'ember'; +import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin'; + +export default Ember.Route.extend(UnauthenticatedRouteMixin, { + activate() { + this.authenticate(); + }, + + authenticate() { + this.get('session').authenticate('authenticator:devise-server-side').catch((error) => { + if(error !== 'unexpected_error') { + window.location.href = '/admin/login'; + } + }); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/not-found.js b/src/api-umbrella/admin-ui/app/routes/not-found.js new file mode 100644 index 000000000..5567520bb --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/not-found.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + renderTemplate() { + Ember.Logger.error('Route not found'); + this.render('not-found'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/base.js b/src/api-umbrella/admin-ui/app/routes/stats/base.js new file mode 100644 index 000000000..485d7c0e7 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/base.js @@ -0,0 +1,50 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + setupController(controller, model) { + controller.set('model', model); + controller.set('queryParamValues', this.get('queryParamValues') || {}); + controller.set('allQueryParamValues', this.paramsFor(this.routeName)); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-analytics').addClass('active'); + }, + + validateParams(params) { + let valid = true; + + let interval = params.interval; + let start = moment(params.start_at); + let end = moment(params.end_at); + + let range = end.unix() - start.unix(); + switch(interval) { + case 'minute': + // 2 days maximum range + if(range > 2 * 24 * 60 * 60) { + valid = false; + bootbox.alert('Your date range is too large for viewing minutely data. Adjust your viewing interval or choose a date range to no more than 2 days.'); + } + + break; + case 'hour': + // 31 day maximum range + if(range > 31 * 24 * 60 * 60) { + valid = false; + bootbox.alert('Your date range is too large for viewing hourly data. Adjust your viewing interval or choose a date range to no more than 31 days.'); + } + + break; + } + + return valid; + }, + + actions: { + queryParamsDidChange(changed, present) { + this._super(...arguments); + this.set('queryParamValues', present); + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/drilldown-legacy.js b/src/api-umbrella/admin-ui/app/routes/stats/drilldown-legacy.js new file mode 100644 index 000000000..79983ca41 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/drilldown-legacy.js @@ -0,0 +1,7 @@ +import Base from './base'; + +export default Base.extend({ + redirect(params) { + this.transitionTo('/stats/drilldown?' + params.legacyParams); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/drilldown.js b/src/api-umbrella/admin-ui/app/routes/stats/drilldown.js new file mode 100644 index 000000000..2be484eb9 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/drilldown.js @@ -0,0 +1,39 @@ +import Base from './base'; +import StatsDrilldown from 'api-umbrella-admin-ui/models/stats/drilldown'; + +export default Base.extend({ + queryParams: { + tz: { + refreshModel: true, + }, + start_at: { + refreshModel: true, + }, + end_at: { + refreshModel: true, + }, + interval: { + refreshModel: true, + }, + query: { + refreshModel: true, + }, + search: { + refreshModel: true, + }, + prefix: { + refreshModel: true, + }, + beta_analytics: { + refreshModel: true, + }, + }, + + model(params) { + if(this.validateParams(params)) { + return StatsDrilldown.find(params); + } else { + return {}; + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/logs-legacy.js b/src/api-umbrella/admin-ui/app/routes/stats/logs-legacy.js new file mode 100644 index 000000000..0a25d913f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/logs-legacy.js @@ -0,0 +1,7 @@ +import Base from './base'; + +export default Base.extend({ + redirect(params) { + this.transitionTo('/stats/logs?' + params.legacyParams); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/logs.js b/src/api-umbrella/admin-ui/app/routes/stats/logs.js new file mode 100644 index 000000000..3e9c4c8b9 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/logs.js @@ -0,0 +1,36 @@ +import Base from './base'; +import StatsLogs from 'api-umbrella-admin-ui/models/stats/logs'; + +export default Base.extend({ + queryParams: { + tz: { + refreshModel: true, + }, + start_at: { + refreshModel: true, + }, + end_at: { + refreshModel: true, + }, + interval: { + refreshModel: true, + }, + query: { + refreshModel: true, + }, + search: { + refreshModel: true, + }, + beta_analytics: { + refreshModel: true, + }, + }, + + model(params) { + if(this.validateParams(params)) { + return StatsLogs.find(params); + } else { + return {}; + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/map-legacy.js b/src/api-umbrella/admin-ui/app/routes/stats/map-legacy.js new file mode 100644 index 000000000..0febff9e9 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/map-legacy.js @@ -0,0 +1,7 @@ +import Base from './base'; + +export default Base.extend({ + redirect(params) { + this.transitionTo('/stats/map?' + params.legacyParams); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/map.js b/src/api-umbrella/admin-ui/app/routes/stats/map.js new file mode 100644 index 000000000..cfc4fb4ea --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/map.js @@ -0,0 +1,37 @@ +import Base from './base'; +import StatsMap from 'api-umbrella-admin-ui/models/stats/map'; + +export default Base.extend({ + queryParams: { + tz: { + refreshModel: true, + }, + start_at: { + refreshModel: true, + }, + end_at: { + refreshModel: true, + }, + query: { + refreshModel: true, + }, + search: { + refreshModel: true, + }, + region: { + refreshModel: true, + }, + beta_analytics: { + refreshModel: true, + }, + }, + + model(params) { + if(this.validateParams(params)) { + return StatsMap.find(params); + } else { + return {}; + } + }, +}); + diff --git a/src/api-umbrella/admin-ui/app/routes/stats/users-legacy.js b/src/api-umbrella/admin-ui/app/routes/stats/users-legacy.js new file mode 100644 index 000000000..625887fb0 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/users-legacy.js @@ -0,0 +1,7 @@ +import Base from './base'; + +export default Base.extend({ + redirect(params) { + this.transitionTo('/stats/users?' + params.legacyParams); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/stats/users.js b/src/api-umbrella/admin-ui/app/routes/stats/users.js new file mode 100644 index 000000000..78720a6ad --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/stats/users.js @@ -0,0 +1,24 @@ +import Base from './base'; + +export default Base.extend({ + queryParams: { + tz: { + refreshModel: true, + }, + start_at: { + refreshModel: true, + }, + end_at: { + refreshModel: true, + }, + query: { + refreshModel: true, + }, + search: { + refreshModel: true, + }, + beta_analytics: { + refreshModel: true, + }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/website-backends/base.js b/src/api-umbrella/admin-ui/app/routes/website-backends/base.js new file mode 100644 index 000000000..4577dc439 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/website-backends/base.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + setupController(controller, model) { + controller.set('model', model); + + $('ul.nav li').removeClass('active'); + $('ul.nav li.nav-config').addClass('active'); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/website-backends/edit.js b/src/api-umbrella/admin-ui/app/routes/website-backends/edit.js new file mode 100644 index 000000000..a99096202 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/website-backends/edit.js @@ -0,0 +1,8 @@ +import Form from './form'; + +export default Form.extend({ + model(params) { + this.clearStoreCache(); + return this.get('store').findRecord('website-backend', params.websiteBackendId); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/routes/website-backends/form.js b/src/api-umbrella/admin-ui/app/routes/website-backends/form.js new file mode 100644 index 000000000..3f6d7b7bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/website-backends/form.js @@ -0,0 +1,6 @@ +import Base from './base'; +import Confirmation from 'api-umbrella-admin-ui/mixins/confirmation'; +import UncachedModel from 'api-umbrella-admin-ui/mixins/uncached-model'; + +export default Base.extend(Confirmation, UncachedModel, { +}); diff --git a/src/api-umbrella/admin-ui/app/routes/website-backends/index.js b/src/api-umbrella/admin-ui/app/routes/website-backends/index.js new file mode 100644 index 000000000..a57fe0956 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/website-backends/index.js @@ -0,0 +1,4 @@ +import Base from './base'; + +export default Base.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/routes/website-backends/new.js b/src/api-umbrella/admin-ui/app/routes/website-backends/new.js new file mode 100644 index 000000000..bb6a73c64 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/routes/website-backends/new.js @@ -0,0 +1,9 @@ +import Form from './form'; + +export default Form.extend({ + model() { + return this.get('store').createRecord('website-backend', { + serverPort: 80, + }); + }, +}); diff --git a/src/api-umbrella/admin-ui/app/serializers/api-user.js b/src/api-umbrella/admin-ui/app/serializers/api-user.js new file mode 100644 index 000000000..c363aa2f5 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/serializers/api-user.js @@ -0,0 +1,8 @@ +import ApplicationSerializer from 'api-umbrella-admin-ui/serializers/application'; +import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin'; + +export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { + attrs: { + settings: { embedded: 'always' }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/serializers/api.js b/src/api-umbrella/admin-ui/app/serializers/api.js new file mode 100644 index 000000000..6b5888f27 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/serializers/api.js @@ -0,0 +1,12 @@ +import ApplicationSerializer from './application'; +import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin'; + +export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { + attrs: { + servers: { embedded: 'always' }, + urlMatches: { embedded: 'always' }, + settings: { embedded: 'always' }, + subSettings: { embedded: 'always' }, + rewrites: { embedded: 'always' }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/serializers/api/settings.js b/src/api-umbrella/admin-ui/app/serializers/api/settings.js new file mode 100644 index 000000000..d4e27577c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/serializers/api/settings.js @@ -0,0 +1,8 @@ +import ApplicationSerializer from 'api-umbrella-admin-ui/serializers/application'; +import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin'; + +export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { + attrs: { + rateLimits: { embedded: 'always' }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/serializers/api/sub-settings.js b/src/api-umbrella/admin-ui/app/serializers/api/sub-settings.js new file mode 100644 index 000000000..ad0c0a000 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/serializers/api/sub-settings.js @@ -0,0 +1,8 @@ +import ApplicationSerializer from '../application'; +import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin'; + +export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { + attrs: { + settings: { embedded: 'always' }, + }, +}); diff --git a/src/api-umbrella/admin-ui/app/serializers/application.js b/src/api-umbrella/admin-ui/app/serializers/application.js new file mode 100644 index 000000000..799eb8529 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/serializers/application.js @@ -0,0 +1,43 @@ +import Ember from 'ember'; +import JSONSerializer from 'ember-data/serializers/json'; + +export default JSONSerializer.extend({ + // Use camel-cased attribute names in the JS models, but underscore the + // attribute names for any server-side communication. + keyForAttribute(attr) { + return Ember.String.underscore(attr); + }, + + // For single records, look for the data under the customizable + // "singlePayloadKey" attribute name on the response. + normalizeSingleResponse(store, primaryModelClass, payload, id, requestType) { + let key = primaryModelClass.singlePayloadKey; + if(key) { + payload = payload[key]; + } + + return this._super(store, primaryModelClass, payload, id, requestType); + }, + + // For multiple records, look for the data under the customizable + // "arrayPayloadKey" attribute name on the response. + normalizeArrayResponse(store, primaryModelClass, payload, id, requestType) { + let key = primaryModelClass.arrayPayloadKey; + if(key) { + payload = payload[key]; + } + + return this._super(store, primaryModelClass, payload, id, requestType); + }, + + // When serializing a record, use the customizable "singlePayloadKey" + // attribute name for the root key. + serializeIntoHash(hash, typeClass, snapshot, options) { + let key = typeClass.singlePayloadKey; + if(key) { + hash[key] = this.serialize(snapshot, options); + } else { + this._super(...arguments); + } + }, +}); diff --git a/src/api-umbrella/admin-ui/app/styles/_base.scss b/src/api-umbrella/admin-ui/app/styles/_base.scss new file mode 100644 index 000000000..e48bf7074 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_base.scss @@ -0,0 +1,414 @@ +body { + padding-top: 60px; +} + +.navbar .navbar-brand { + font-weight: bold; +} + +.button-actions { + text-align: right; + margin-bottom: 16px; +} + +.button-actions-down { + position: relative; + height: 0px; + margin: 0px; + z-index: 999; +} + +#results_table .table th, +#results_table .table td { + white-space: nowrap; +} + +#results_table .table thead th { + color: $link-color; +} + +table.table thead .sorting, +table.table thead .sorting_asc, +table.table thead .sorting_desc { + background: none; +} + +table.table thead .sorting:after, +table.table thead .sorting_asc:after, +table.table thead .sorting_desc:after { + opacity: 1.0; + font-family: FontAwesome; + color: $gray-lighter; + margin-left: 8px; +} + +table.table thead .sorting_asc:after, +table.table thead .sorting_desc:after { + color: $link-color; +} + +table.table thead .sorting:after { + content: "\F0DC"; +} + +table.table thead .sorting_asc:after { + content: "\F0DE"; +} + +table.table thead .sorting_desc:after { + content: "\F0DD"; +} + +div.dataTables_info { + padding: 0px; + font-size: 13px; + color: $text-color-light; +} + +div.dataTables_paginate { + text-align: center !important; + float: none; +} + +div.dataTables_length { + text-align: right; + + label { + float: none; + font-size: 13px; + color: $text-color-light; + } + + select { + font-size: 13px; + } +} + +div.dataTables_wrapper { + position: relative; + + div.dataTables_filter { + label { + float: left; + } + + input { + margin-left: 0px; + } + } +} + +.dataTables_processing { + position: absolute; + top: 50%; + left: 50%; + width: 250px; + height: 60px; + margin-left: -125px; + margin-top: -30px; + border: 1px solid #ddd; + text-align: center; + color: $gray-light; + font-size: 32px; + line-height: 60px; + background-color: white; +} + +.form-horizontal.condensed .form-group { + margin-bottom: 5px; +} + +fieldset { + margin: 12px 0px 32px 0px; + padding: 6px 0px 6px 12px; +} + +fieldset.collapsible { + margin-bottom: 12px; + + .collapse.in { + margin-bottom: 24px; + } +} + +legend { + margin-bottom: 2px; + border-bottom-width: 1px; + border-color: $gray-light; + margin-left: -12px; + padding: 6px 10px 3px 2px; + line-height: 22px; + background-color: #fff; +} + +legend a { + display: block; +} + +.fieldset-note { + font-size: 85%; + color: $text-color-light; + margin-bottom: 4px; +} + +fieldset .table { + margin-bottom: 4px; +} + +.control-group.error .help-block { + margin-top: -8px; + font-style: italic; +} + +.form-horizontal .control-group.error .help-block { + margin-top: 0px; +} + +.error-messages { + h4 { + margin-bottom: 4px; + } + + ul { + margin-bottom: 0px; + + ul { + margin-top: 10px; + margin-bottom: 10px; + } + } + + li { + p { + margin: 10px 0px 0px 0px; + } + + p:first-child { + margin: 0px; + } + } +} + +.arrow { + margin-top: 24px; + text-align: center; + color: $gray-light; + font-size: 10px; + line-height: 10px; +} + +.arrow i { + color: lighten($gray-light, 16%); +} + +.arrow-vertical { + margin: 0px; +} + +.arrow-vertical i { + vertical-align: middle; +} + +a { + .fa { + text-decoration: none; + margin-right: 3px; + } +} + +.fa-space-right { + margin-right: 3px; +} + +.table .table-row-actions { + text-align: right; + white-space: nowrap; + + a { + margin-left: 24px; + } +} + +a.remove-action { + font-size: 12px; + color: $brand-danger; +} + +.table-actions { +} + +.form-extra-actions { + margin-top: 20px; +} + +table input { + margin-bottom: 0px !important; +} + +legend a:hover { + text-decoration: none; +} + +a[data-toggle='collapse'] .fa-caret-down:before { + display: block; + width: 16px; + text-align: center; +} + +a[data-toggle='collapse'].collapsed .fa-caret-down:before { + content: "\f0da"; +} + +a:hover { + cursor: pointer; +} + +.api-key { + font-family: $font-family-monospace; +} + +.record-details { + text-align: right; + font-size: 11px; + line-height: 13px; + color: $text-color-lighter; +} + +th.reorder-handle, +td.reorder-handle { + display: none; + width: 60px; + text-align: center; + cursor: move; +} + +.reorder-active { + th.reorder-handle, + td.reorder-handle { + display: table-cell; + } +} + +.reorder-placeholder th, +.reorder-placeholder td { + background-color: $state-warning-bg !important; +} + +.form-horizontal .ace_editor { + height: 96px; + border: 1px solid $input-border; + border-radius: $input-border-radius; +} + +.control-label a[rel='tooltip'] { + margin-left: 8px; +} + +.qtip-bootstrap { + font-size: 12px; + max-width: 320px; + + pre { + font-size: 11px; + line-height: 15px; + } +} + +.qtip-wide { + max-width: 700px; +} + +.qtip-forced-wide { + min-width: 400px; + max-width: 600px; +} + +.qtip-content p:last-child { + margin-bottom: 0px; +} + +.query-syntax-help .example { + text-align: right; +} + +.query-syntax-help td.example { + font-style: italic; + font-size: 11px; +} + +.table-small { + font-size: 13px; +} + +.table tr.empty td { + text-align: center; + font-size: 12px; + font-style: italic; + color: $text-color-light; +} + +.table th.text-center, +.table td.text-center { + text-align: center; +} + +.table tr.line-bottom th, +.table tr.line-bottom td { + border-top: none; + border-bottom: 1px solid $table-border-color; +} + +.diff { + del { + text-decoration: none; + background: #fdd; + } + + ins { + text-decoration: none; + background: #dfd; + } +} + +.dropdown-menu { + .fa { + /* Make the icons a consistent width so the menu text lines up */ + width: 16px; + text-align: center; + } +} + +.publish-toggle-checkboxes { + margin: -28px 0px 32px 17px; + text-align: center; + width: 80px; + font-size: 85%; +} + +.control-group-negative-top { + margin-top: -16px; +} + +#version_footer .row-fluid { + border-top: 1px solid $gray-lighter; + text-align: center; + margin-top: 3em; + margin-bottom: 1em; + padding-top: 1em; + font-size: 11px; + color: $text-color-lighter; +} + +// For when we want to submit a form with the enter key, but don't want a real +// submit button visible in the form (eg, in modals where the submit button is +// in the modal footer and not inside the
tag): +// http://stackoverflow.com/q/477691 +.hidden-submit-button { + position: absolute; + left: -9999px; + height: 0px; + width: 0px; + border: none; + padding: 0px; + margin: 0px; +} diff --git a/src/api-umbrella/admin-ui/app/styles/_busy-blocker.scss b/src/api-umbrella/admin-ui/app/styles/_busy-blocker.scss new file mode 100644 index 000000000..d66011304 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_busy-blocker.scss @@ -0,0 +1,71 @@ +@keyframes fadeIn { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + +.fade-in { + animation-name: fadeIn; +} + +@keyframes fadeOut { + 0% {opacity: 1;} + 100% {opacity: 0;} +} + +.fade-out { + animation-name: fadeOut; +} + +@keyframes spinnerRotation { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(359deg); + } +} + +.busy-blocker { display: none; } + +.busy-blocker__animation { + animation: spinnerRotation 0.6s infinite linear; + border: 6px solid rgba(255, 255, 255, 0.5); + border-top: 6px solid rgba(255, 255, 255, 0.9); + border-radius: 100%; + height: 45px; + margin: auto; + width: 45px; +} + +.busy-blocker__bg { + animation-fill-mode: both; + background: rgba(0, 0, 0, 0.75); + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; + z-index: $zindex-modal-background; +} + +.busy-blocker__content { + animation-duration: 0.2s; + animation-fill-mode: both; + color: #eee; + left: 50%; + margin-left: -100px; + margin-top: -40px; + position: fixed; + text-align: center; + top: 50%; + width: 200px; + z-index: $zindex-modal; +} + +.busy-blocker__content p { + font-size: 18px; + margin: 12px 0px 0px 0px; + font-weight: bold; + color: #f7f7f7; +} diff --git a/src/api-umbrella/admin-ui/app/styles/_input-xs.scss b/src/api-umbrella/admin-ui/app/styles/_input-xs.scss new file mode 100644 index 000000000..0b19e4dab --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_input-xs.scss @@ -0,0 +1,5 @@ +$padding-xs-vertical: 3px; +$padding-xs-horizontal: 5px; +$input-height-xs: (floor($font-size-small * $line-height-small) + ($padding-xs-vertical * 2) + 2) !default; + +@include input-size('.input-xs', $input-height-xs, $padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $input-border-radius-small); diff --git a/src/api-umbrella/admin-ui/app/styles/_noscript.scss b/src/api-umbrella/admin-ui/app/styles/_noscript.scss new file mode 100644 index 000000000..f77bbcffd --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_noscript.scss @@ -0,0 +1,9 @@ +noscript { + margin: 0px 20px 20px 20px; +} + +noscript p { + text-align: center; + font-size: 18px; + margin: 0px; +} diff --git a/src/api-umbrella/admin-ui/app/styles/_selectize.scss b/src/api-umbrella/admin-ui/app/styles/_selectize.scss new file mode 100644 index 000000000..709e0fcfb --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_selectize.scss @@ -0,0 +1,3 @@ +.selectize-dropdown { + z-index: $zindex-modal + 10; +} diff --git a/src/api-umbrella/admin-ui/app/styles/_stats.scss b/src/api-umbrella/admin-ui/app/styles/_stats.scss new file mode 100644 index 000000000..c34399c0f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_stats.scss @@ -0,0 +1,130 @@ +.toggle-facet-table { + font-size: 11px; +} + +.facet-table { + font-size: 11px; + text-align: left; + + th, + td { + padding: 2px !important; + } + + th.value, + td.value { + text-align: right; + } +} + +#map_container { + width: 640px; + margin: 0px auto 20px auto; +} + +.advanced-filter { + .help-block { + font-size: 85%; + line-height: 110%; + margin-top: 2px; + } +} + +.filter-times { + text-align: right; + + #interval_buttons { + display: inline-block; + margin: 4px 30px 0px 0px; + } + + #reportrange { + display: inline-block; + } +} + +.number-highlights { + margin: 20px 0px; +} + +.number-highlight { + text-align: center; + + strong { + font-size: 54px; + line-height: 100%; + font-weight: 100; + display: block; + + .units { + font-size: 28px; + line-height: 100%; + } + } + + small { + font-weight: bold; + font-size: 14px; + } +} + +#filter_toggle .fa { + width: 8px; +} + +#filters_ui { + max-width: 960px; + + .filter-type-toggle { + font-size: 12px; + text-align: right; + } + + #search_field { + .input-append { + display: block; + + .full-width-input, + .full-width-submit { + display: table-cell; + } + + .full-width-input { + width: 100%; + } + + .full-width-input input { + width: 100%; + box-sizing: border-box; + height: 26px; + } + } + } +} + +.query-builder { + .btn-xs { + font-size: 12px; + padding: 1px 5px; + } + + .rule-container input[type=number], + .rule-container input[type=text], + .rule-container select { + font-size: 11px; + height: 24px; + margin: 0px; + box-sizing: border-box; + padding: 1px 3px; + } + + .rule-container input[type=number] { + width: 102px; + } + + .rules-group-container { + background: #edf5fc; + background: rgba(237, 245, 252, 0.5); + border-color: #96bedc; + } +} diff --git a/src/api-umbrella/admin-ui/app/styles/_variables_post.scss b/src/api-umbrella/admin-ui/app/styles/_variables_post.scss new file mode 100644 index 000000000..8d8fe42bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_variables_post.scss @@ -0,0 +1,10 @@ +$headings-color: $text-color; + +$navbar-default-bg: lighten($gray-base, 23%); +$navbar-default-link-hover-bg: darken($navbar-default-bg, 10%); +$navbar-default-link-active-bg: $navbar-default-link-hover-bg; + +$gray-lighter: lighten($gray-base, 85%); + +$text-color-light: lighten($text-color, 16%); +$text-color-lighter: lighten($text-color, 28%); diff --git a/src/api-umbrella/admin-ui/app/styles/_variables_pre.scss b/src/api-umbrella/admin-ui/app/styles/_variables_pre.scss new file mode 100644 index 000000000..03dfeffad --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/_variables_pre.scss @@ -0,0 +1,7 @@ +$text-color: #313131; + +// Use the default bootstrap padding & heights, rather than the larger defaults +// from the Cerulean theme. +$padding-base-vertical: 6px; +$padding-large-vertical: 10px; +$modal-inner-padding: 15px; diff --git a/src/api-umbrella/admin-ui/app/styles/app.scss b/src/api-umbrella/admin-ui/app/styles/app.scss new file mode 100644 index 000000000..f09fbf818 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/styles/app.scss @@ -0,0 +1,15 @@ +@import "_variables_pre.scss"; +@import "bower_components/bootswatch/cerulean/_variables.scss"; +@import "_variables_post.scss"; +@import "bower_components/bootstrap-sass/assets/stylesheets/_bootstrap.scss"; +@import "bower_components/bootswatch/cerulean/_bootswatch.scss"; +@import "bower_components/font-awesome/scss/font-awesome.scss"; + +@import "bower_components/jQuery-QueryBuilder/dist/scss/default.scss"; +@import "bower_components/bootstrap-daterangepicker/daterangepicker.scss"; +@import "_noscript.scss"; +@import "_busy-blocker.scss"; +@import "_input-xs.scss"; +@import "_base.scss"; +@import "_stats.scss"; +@import "_selectize.scss"; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/.gitkeep b/src/api-umbrella/admin-ui/app/templates/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/templates/.gitkeep rename to src/api-umbrella/admin-ui/app/templates/.gitkeep diff --git a/src/api-umbrella/admin-ui/app/templates/admin-groups/edit.hbs b/src/api-umbrella/admin-ui/app/templates/admin-groups/edit.hbs new file mode 100644 index 000000000..57403ecb4 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/admin-groups/edit.hbs @@ -0,0 +1,2 @@ +

Edit Admin Group

+{{admin-groups/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/admin-groups/index.hbs b/src/api-umbrella/admin-ui/app/templates/admin-groups/index.hbs new file mode 100644 index 000000000..6131895ed --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/admin-groups/index.hbs @@ -0,0 +1,7 @@ +

Admin Groups

+ +
+ {{#link-to "admin_groups.new" class="btn btn-primary"}} Add New Admin Group{{/link-to}} +
+ +{{admin-groups/index-table}} diff --git a/src/api-umbrella/admin-ui/app/templates/admin-groups/new.hbs b/src/api-umbrella/admin-ui/app/templates/admin-groups/new.hbs new file mode 100644 index 000000000..5a546d74e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/admin-groups/new.hbs @@ -0,0 +1,2 @@ +

Add Admin Group

+{{admin-groups/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/admins/edit.hbs b/src/api-umbrella/admin-ui/app/templates/admins/edit.hbs new file mode 100644 index 000000000..6f3ab36ed --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/admins/edit.hbs @@ -0,0 +1,2 @@ +

Edit Admin

+{{admins/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/admins/index.hbs b/src/api-umbrella/admin-ui/app/templates/admins/index.hbs new file mode 100644 index 000000000..029a9e433 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/admins/index.hbs @@ -0,0 +1,7 @@ +

Admins

+ +
+ {{#link-to "admins.new" class="btn btn-primary"}} Add New Admin{{/link-to}} +
+ +{{admins/index-table}} diff --git a/src/api-umbrella/admin-ui/app/templates/admins/new.hbs b/src/api-umbrella/admin-ui/app/templates/admins/new.hbs new file mode 100644 index 000000000..735cbc3e5 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/admins/new.hbs @@ -0,0 +1,2 @@ +

Add Admin

+{{admins/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-scopes/edit.hbs b/src/api-umbrella/admin-ui/app/templates/api-scopes/edit.hbs new file mode 100644 index 000000000..5569f5eed --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/api-scopes/edit.hbs @@ -0,0 +1,2 @@ +

Edit API Scope

+{{api-scopes/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-scopes/index.hbs b/src/api-umbrella/admin-ui/app/templates/api-scopes/index.hbs new file mode 100644 index 000000000..1fc8e3740 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/api-scopes/index.hbs @@ -0,0 +1,7 @@ +

API Scopes

+ +
+ {{#link-to "api_scopes.new" class="btn btn-primary"}} Add New API Scope{{/link-to}} +
+ +{{api-scopes/index-table}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-scopes/new.hbs b/src/api-umbrella/admin-ui/app/templates/api-scopes/new.hbs new file mode 100644 index 000000000..2da277c59 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/api-scopes/new.hbs @@ -0,0 +1,2 @@ +

Add API Scope

+{{api-scopes/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-users/edit.hbs b/src/api-umbrella/admin-ui/app/templates/api-users/edit.hbs new file mode 100644 index 000000000..acb5b7f34 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/api-users/edit.hbs @@ -0,0 +1,2 @@ +

Edit API User

+{{api-users/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-users/index.hbs b/src/api-umbrella/admin-ui/app/templates/api-users/index.hbs new file mode 100644 index 000000000..cead37668 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/api-users/index.hbs @@ -0,0 +1,7 @@ +

API Users

+ +
+ {{#link-to "api_users.new" class="btn btn-primary"}} Add New API User{{/link-to}} +
+ +{{api-users/index-table}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-users/new.hbs b/src/api-umbrella/admin-ui/app/templates/api-users/new.hbs new file mode 100644 index 000000000..8ab7b4081 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/api-users/new.hbs @@ -0,0 +1,2 @@ +

Add API User

+{{api-users/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/apis/edit.hbs b/src/api-umbrella/admin-ui/app/templates/apis/edit.hbs new file mode 100644 index 000000000..e294646cc --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/apis/edit.hbs @@ -0,0 +1,2 @@ +

Edit API

+{{apis/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/apis/index.hbs b/src/api-umbrella/admin-ui/app/templates/apis/index.hbs new file mode 100644 index 000000000..a2bae09cd --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/apis/index.hbs @@ -0,0 +1,7 @@ +

API Backends

+ +
+ {{#link-to "apis.new" class="btn btn-primary"}} Add API Backend{{/link-to}} +
+ +{{apis/index-table}} diff --git a/src/api-umbrella/admin-ui/app/templates/apis/new.hbs b/src/api-umbrella/admin-ui/app/templates/apis/new.hbs new file mode 100644 index 000000000..79aaa23b6 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/apis/new.hbs @@ -0,0 +1,2 @@ +

Add API

+{{apis/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/application.hbs b/src/api-umbrella/admin-ui/app/templates/application.hbs new file mode 100644 index 000000000..799c2b6e6 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/application.hbs @@ -0,0 +1,69 @@ +{{#if session.isAuthenticated}} + + +
+ {{outlet}} +
+ + + + {{ember-load-remover}} + {{busy-blocker}} +{{/if}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/.gitkeep b/src/api-umbrella/admin-ui/app/templates/components/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/views/.gitkeep rename to src/api-umbrella/admin-ui/app/templates/components/.gitkeep diff --git a/src/api-umbrella/admin-ui/app/templates/components/admin-groups/index-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/admin-groups/index-table.hbs new file mode 100644 index 000000000..52f6a684f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/admin-groups/index-table.hbs @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/admin-groups/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/admin-groups/record-form.hbs new file mode 100644 index 000000000..84107b1e4 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/admin-groups/record-form.hbs @@ -0,0 +1,42 @@ +{{error-messages model=model}} + + + {{#fields-for model=model style="horizontal" as |f|}} +
+ {{f.text-field "name" label="Group Name"}} + {{f.checkboxes-field "apiScopeIds" label="Scopes" options=apiScopeOptions}} + {{f.checkboxes-field "permissionIds" label="Permissions" options=permissionOptions}} + + {{#if model.id}} + {{#f.static-field "admins" label="Admins"}} + {{#if model.admins}} +
    + {{#each model.admins as |admin|}} +
  • {{#link-to "admins.edit" admin.id}}{{admin.username}}{{/link-to}} (Last Login: {{#if admin.last_sign_in_at}}{{format-date admin.last_sign_in_at}}{{else}}Never{{/if}})
  • + {{/each}} +
+ {{else}} + None + {{/if}} + {{/f.static-field}} + {{/if}} +
+ +
+
+ +
+
+ {{#if model.id}} + Created: {{format-date model.createdAt}} by {{model.creator.username}}
+ Last Updated: {{format-date model.updatedAt}} by {{model.updater.username}}
+ {{/if}} +
+
+ {{#if model.id}} + + {{/if}} + {{/fields-for}} +
diff --git a/src/api-umbrella/admin-ui/app/templates/components/admins/index-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/admins/index-table.hbs new file mode 100644 index 000000000..e05f53a67 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/admins/index-table.hbs @@ -0,0 +1,7 @@ +
+
+
+ + diff --git a/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs new file mode 100644 index 000000000..5b9f2de64 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs @@ -0,0 +1,56 @@ +{{error-messages model=model}} + +
+ {{#fields-for model=model style="horizontal" as |f|}} +
+ User Info + + {{f.text-field "username" label="Username"}} + {{f.textarea-field "notes" label="Notes"}} + {{#if model.email}} + {{f.static-field "email" label="E-mail"}} + {{/if}} + {{#if model.name}} + {{f.static-field "name" label="Name"}} + {{/if}} +
+ + {{#if model.authenticationToken}} +
+ Admin API Access + + {{#f.static-field "authenticationToken" label="Admin API Token"}} + {{model.authenticationToken}} + {{/f.static-field}} +
+ {{/if}} + +
+ Permissions + + {{f.checkboxes-field "groupIds" label="Groups" options=groupOptions}} + {{#if currentAdmin.superuser}} + {{f.checkbox-field "superuser" label="Superuser"}} + {{/if}} +
+ +
+
+ +
+
+ {{#if model.id}} + Created: {{format-date model.createdAt}} by {{model.creator.username}}
+ Last Updated: {{format-date model.updatedAt}} by {{model.updater.username}}
+ Last Login: {{format-date model.lastSignInAt}} from {{model.lastSignInIp}} via {{model.lastSignInProvider}}
+ Logged in: {{model.signInCount}} times
+ {{/if}} +
+
+ {{#if model.id}} + + {{/if}} + {{/fields-for}} +
diff --git a/src/api-umbrella/admin-ui/app/templates/components/api-scopes/index-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/api-scopes/index-table.hbs new file mode 100644 index 000000000..52f6a684f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/api-scopes/index-table.hbs @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/api-scopes/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/api-scopes/record-form.hbs new file mode 100644 index 000000000..568266bf6 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/api-scopes/record-form.hbs @@ -0,0 +1,28 @@ +{{error-messages model=model}} + +
+ {{#fields-for model=model style="horizontal" as |f|}} +
+ {{f.text-field "name" label="Name"}} + {{f.text-field "host" label="Host"}} + {{f.text-field "pathPrefix" label="Path Prefix"}} +
+ +
+
+ +
+
+ {{#if model.id}} + Created: {{format-date model.createdAt}} by {{model.creator.username}}
+ Last Updated: {{format-date model.updatedAt}} by {{model.updater.username}}
+ {{/if}} +
+
+ {{#if model.id}} + + {{/if}} + {{/fields-for}} +
diff --git a/src/api-umbrella/admin-ui/app/templates/components/api-users/index-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/api-users/index-table.hbs new file mode 100644 index 000000000..52f6a684f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/api-users/index-table.hbs @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/api-users/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/api-users/record-form.hbs new file mode 100644 index 000000000..626bb3667 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/api-users/record-form.hbs @@ -0,0 +1,70 @@ +{{error-messages model=model}} + +
+ {{#fields-for model=model style="horizontal" as |f|}} +
+ User Info + + {{f.text-field "email" label="E-mail"}} + {{f.text-field "firstName" label="First Name"}} + {{f.text-field "lastName" label="Last Name"}} + {{f.textarea-field "useDescription" label="Purpose"}} + + {{#if model.id}} + {{#f.static-field "createdAt" label="Signed Up"}} + {{format-date model.createdAt}} + {{/f.static-field}} + {{#f.static-field "apiKey" label="API Key"}} + {{#if model.apiKey}} + {{model.apiKeyPreview}} {{t "admin.reveal_action"}} + {{else}} + {{model.apiKeyPreview}} + {{/if}} + {{/f.static-field}} + {{f.static-field "id" label="User ID"}} + {{f.static-field "registrationSource" label="Registration Source"}} + {{else}} + {{f.checkbox-field "termsAndConditions" label=(html-safe "User agrees to the terms and conditions")}} + {{f.checkbox-field "sendWelcomeEmail" label="Send user welcome e-mail with API key information"}} + {{/if}} +
+ +
+ Rate Limiting + {{apis/settings/rate-limit-fields model=model.settings}} + {{f.select-field "throttleByIp" label="Limit By" options=throttleByIpOptions}} +
+ +
+ Permissions + + {{f.selectize-field "rolesString" label="Roles" options=roleOptions}} + {{apis/settings/allowed-ips-fields model=model.settings}} + {{apis/settings/allowed-referers-fields model=model.settings}} + {{f.select-field "enabled" label="Account Enabled" options=enabledOptions}} +
+ +
+
+ +
+
+ Created: {{format-date model.createdAt}}{{#if model.creator}} by {{model.creator.username}}{{/if}}
+ Last Updated: {{format-date model.updatedAt}}{{#if model.updater}} by {{model.updater.username}}{{/if}}
+ E-mail Verified: {{model.emailVerified}}
+ {{#if model.registrationIp}} + Registration IP: {{model.registrationIp}}
+ {{/if}} + {{#if model.registrationUserAgent}} + Registration User Agent: {{model.registrationUserAgent}}
+ {{/if}} + {{#if model.registrationReferer}} + Registration Referer: {{model.registrationReferer}}
+ {{/if}} + {{#if model.registrationOrigin}} + Registration Origin: {{model.registrationOrigin}}
+ {{/if}} +
+
+ {{/fields-for}} +
diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/index-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/index-table.hbs new file mode 100644 index 000000000..65a3eecc8 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/index-table.hbs @@ -0,0 +1,6 @@ +
+
+
+
+ +
diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/record-form.hbs new file mode 100644 index 000000000..db8c2af51 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/record-form.hbs @@ -0,0 +1,111 @@ +{{error-messages model=model}} + +
+ {{#fields-for model=model style="vertical" as |f|}} + {{f.text-field "name" label=(t "mongoid.attributes.api.name")}} + +
+ {{t "admin.api.servers.legend"}} +

{{t "admin.api.servers.note"}}

+ {{f.select-field "backendProtocol" label=(t "mongoid.attributes.api.backend_protocol") options=backendProtocolOptions}} + + {{apis/server-table model=model}} +
+ +
+ {{t "admin.api.host.legend"}} +

{{t "admin.api.host.note"}}

+
+
+ {{f.text-field "frontendHost" label="Frontend Host"}} +
+
+ +
rewrite to
+
+
+ {{f.text-field "backendHost" label="Backend Host" placeholder="api.example.com"}} +
+
+
+ {{/fields-for}} + +
+ {{t "admin.api.url_matches.legend"}} +

{{t "admin.api.url_matches.note"}}

+ {{apis/url-match-table model=model}} +
+ +
+ +
+ {{#fields-for model=model.settings style="horizontal-wide-labels" as |f|}} + {{f.text-field "appendQueryString" label="Append Query String Parameters" placeholder="param1=value¶m2=value"}} + {{f.textarea-field "headersString" label="Set Request Headers" placeholder="X-Example-Header: value"}} + {{f.text-field "httpBasicAuth" label="HTTP Basic Authentication" placeholder="username:password"}} + {{/fields-for}} + + {{apis/settings/common-fields model=model.settings}} +
+
+ +
+ +
+

{{t "admin.api.sub_settings.note"}}

+ {{apis/sub-settings-table model=model}} +
+
+ +
+ +
+

{{t "admin.api.rewrites.note"}}

+ {{apis/rewrite-table model=model}} +
+
+ +
+ +
+ {{#fields-for model=model style="horizontal-wide-labels" as |f|}} + {{f.select-field "balanceAlgorithm" label="Balance Algorithm" options=balanceAlgorithmOptions}} + {{/fields-for}} + + {{#fields-for model=model.settings.errorTemplates style="horizontal-wide-labels" as |f|}} +

{{t "mongoid.attributes.api/settings.error_templates"}}

+ {{f.ace-field "json" label=(t "admin.api.settings.error_templates_json") tooltip=(t "admin.api.settings.error_templates_json_tooltip_markdown") mode="json"}} + {{f.ace-field "xml" label=(t "admin.api.settings.error_templates_xml") tooltip=(t "admin.api.settings.error_templates_xml_tooltip_markdown") mode="xml"}} + {{f.ace-field "csv" label=(t "admin.api.settings.error_templates_csv") tooltip=(t "admin.api.settings.error_templates_csv_tooltip_markdown") mode="text"}} + {{/fields-for}} + + {{#fields-for model=model.settings.errorDataYamlStrings style="horizontal-wide-labels" as |f|}} +

{{t "mongoid.attributes.api/settings.error_data"}}

+ {{f.ace-field "common" label=(t "admin.api.settings.error_data_common") tooltip=(t "admin.api.settings.error_data_common_tooltip_markdown") mode="yaml"}} + {{f.ace-field "api_key_missing" label=(t "admin.api.settings.error_data_api_key_missing") tooltip=(t "admin.api.settings.error_data_api_key_missing_tooltip_markdown") mode="yaml"}} + {{f.ace-field "api_key_invalid" label=(t "admin.api.settings.error_data_api_key_invalid") tooltip=(t "admin.api.settings.error_data_api_key_invalid_tooltip_markdown") mode="yaml"}} + {{f.ace-field "api_key_disabled" label=(t "admin.api.settings.error_data_api_key_disabled") tooltip=(t "admin.api.settings.error_data_api_key_disabled_tooltip_markdown") mode="yaml"}} + {{f.ace-field "api_key_unauthorized" label=(t "admin.api.settings.error_data_api_key_unauthorized") tooltip=(t "admin.api.settings.error_data_api_key_unauthorized_tooltip_markdown") mode="yaml"}} + {{f.ace-field "over_rate_limit" label=(t "admin.api.settings.error_data_over_rate_limit") tooltip=(t "admin.api.settings.error_data_over_rate_limit_tooltip_markdown") mode="yaml"}} + {{f.ace-field "https_required" label=(t "admin.api.settings.error_data_https_required") tooltip=(t "admin.api.settings.error_data_https_required_tooltip_markdown") mode="yaml"}} + {{/fields-for}} +
+
+ +
+
+ +
+
+ {{#if model.id}} + Created: {{format-date model.createdAt}} by {{model.creator.username}}
+ Last Updated: {{format-date model.updatedAt}} by {{model.updater.username}}
+ {{/if}} +
+
+ {{#if model.id}} + + {{/if}} +
diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/rewrite-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/rewrite-form.hbs new file mode 100644 index 000000000..e379dee53 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/rewrite-form.hbs @@ -0,0 +1,31 @@ +{{#bs-modal open=openModal submitAction=(action "submit") closedAction=(action "closed") title=modalTitle body=false footer=false backdropClose=false size="lg"}} + {{#bs-modal-body}} +
+ {{#fields-for model=bufferedModel style="vertical" as |f|}} +
+
+ {{f.select-field "matcherType" label="Matcher Type" options=matcherTypeOptions}} +
+
+
+
+ {{f.select-field "httpMethod" label="HTTP Method" options=httpMethodOptions}} +
+
+ {{f.text-field "frontendMatcher" label="Frontend Matcher" placeholder="/example/"}} +
+
+
+ rewrite to +
+
+
+ {{f.text-field "backendReplacement" label="Backend Replacement" placeholder="/example/"}} +
+
+ {{/fields-for}} + +
+ {{/bs-modal-body}} + {{bs-modal-footer closeTitle="Cancel" submitTitle="OK"}} +{{/bs-modal}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/rewrite-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/rewrite-table.hbs new file mode 100644 index 000000000..f2b73c30c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/rewrite-table.hbs @@ -0,0 +1,43 @@ +{{apis/rewrite-form model=rewriteModel collection=model.rewrites openModal=openModal}} + + + + + + + + + + + + + + {{#if model.rewrites}} + {{#each model.rewrites as |rewrite|}} + + + + + + + + + {{/each}} + {{else}} + + {{/if}} + +
Matching TypeHTTP MethodFromTo
{{rewrite.matcherType}}{{rewrite.httpMethod}}{{rewrite.frontendMatcher}}{{rewrite.backendReplacement}} + {{t "admin.edit"}} + {{t "admin.remove"}} +
{{t "admin.api.rewrites.empty_list" add=(t "admin.api.rewrites.add")}}
+
+
+ +
+
+ {{#if isReorderable}} + + {{/if}} +
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/server-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/server-form.hbs new file mode 100644 index 000000000..5327d8c86 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/server-form.hbs @@ -0,0 +1,18 @@ +{{#bs-modal open=openModal openAction=(action "open") submitAction=(action "submit") closedAction=(action "closed") title=modalTitle body=false footer=false backdropClose=false}} + {{#bs-modal-body}} +
+ {{#fields-for model=bufferedModel style="vertical" as |f|}} +
+
+ {{f.text-field "host" label="Host" placeholder="api.example.com"}} +
+
+ {{f.text-field "port" label="Port"}} +
+
+ {{/fields-for}} + +
+ {{/bs-modal-body}} + {{bs-modal-footer closeTitle="Cancel" submitTitle="OK"}} +{{/bs-modal}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/server-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/server-table.hbs new file mode 100644 index 000000000..4bb462135 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/server-table.hbs @@ -0,0 +1,26 @@ +{{apis/server-form model=serverModel collection=model.servers apiBackendProtocol=model.backendProtocol apiBackendHost=model.backendHost openModal=openModal}} + + + + + + + + + + {{#if model.servers}} + {{#each model.servers as |server|}} + + + + + {{/each}} + {{else}} + + {{/if}} + +
{{t "admin.api.servers.server"}}
{{model.backendProtocol}}://{{server.hostWithPort}} + {{t "admin.edit"}} + {{t "admin.remove"}} +
{{t "admin.api.servers.empty_list" add=(t "admin.api.servers.add")}}
+ diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/settings/allowed-ips-fields.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/allowed-ips-fields.hbs new file mode 100644 index 000000000..a1d4e152a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/allowed-ips-fields.hbs @@ -0,0 +1,3 @@ +{{#fields-for model=model style="horizontal" as |f|}} + {{f.textarea-field "allowedIpsString" label=(t "mongoid.attributes.api/settings.allowed_ips") tooltip=(t "admin.api.settings.allowed_ips_tooltip_markdown") placeholder="10.0.0.0/8"}} +{{/fields-for}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/settings/allowed-referers-fields.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/allowed-referers-fields.hbs new file mode 100644 index 000000000..af13cef22 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/allowed-referers-fields.hbs @@ -0,0 +1,3 @@ +{{#fields-for model=model style="horizontal" as |f|}} + {{f.textarea-field "allowedReferersString" label=(t "mongoid.attributes.api/settings.allowed_referers") tooltip=(t "admin.api.settings.allowed_referers_tooltip_markdown") placeholder="*.example.com/*"}} +{{/fields-for}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/settings/common-fields.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/common-fields.hbs new file mode 100644 index 000000000..a03e3faec --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/common-fields.hbs @@ -0,0 +1,19 @@ +{{#fields-for model=model style="horizontal-wide-labels" as |f|}} + {{f.select-field "requireHttps" label=(t "admin.api.settings.require_https") tooltip=(t "admin.api.settings.require_https_tooltip_markdown") options=requireHttpsOptions}} + {{f.select-field "disableApiKey" label=(t "admin.api.settings.disable_api_key") options=disableApiKeyOptions}} + {{f.select-field "apiKeyVerificationLevel" label=(t "admin.api.settings.api_key_verification_level") options=apiKeyVerificationLevelOptions}} + {{f.selectize-field "requiredRolesString" label=(t "admin.api.settings.required_roles") tooltip=(t "admin.api.settings.required_roles_tooltip_markdown") options=roleOptions}} + {{#if isSubSettings}} + {{f.checkbox-field "requiredRolesOverride" label=(t "admin.api.settings.required_roles_override") tooltip=(t "admin.api.settings.required_roles_override_tooltip_markdown")}} + {{/if}} + {{f.checkboxes-field "passApiKey" label=(t "admin.api.settings.pass_api_key") tooltip=(t "admin.api.settings.pass_api_key_tooltip_markdown") options=passApiKeyOptions}} + + {{apis/settings/rate-limit-fields model=model style="horizontal-wide-labels"}} + {{#if model.disableApiKey}} + {{f.select-field "anonymousRateLimitBehavior" label="Anonymous Rate Limit Behavior" options=anonymousRateLimitBehaviorOptions}} + {{f.select-field "authenticatedRateLimitBehavior" label="Authenticated Rate Limit Behavior" options=authenticatedRateLimitBehaviorOptions}} + {{/if}} + + {{f.textarea-field "defaultResponseHeadersString" label=(t "admin.api.settings.default_response_headers") tooltip=(t "admin.api.settings.default_response_headers_tooltip_markdown") placeholder="X-Example-Header: value"}} + {{f.textarea-field "overrideResponseHeadersString" label=(t "admin.api.settings.override_response_headers") tooltip=(t "admin.api.settings.override_response_headers_tooltip_markdown") placeholder="X-Example-Header: value"}} +{{/fields-for}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/settings/rate-limit-fields.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/rate-limit-fields.hbs new file mode 100644 index 000000000..1529590f6 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/settings/rate-limit-fields.hbs @@ -0,0 +1,60 @@ +{{#fields-for model=model style=(if style style "horizontal") as |f|}} + {{f.select-field "rateLimitMode" label="Rate Limit" options=rateLimitModeOptions}} + + {{#if model.isRateLimitModeCustom}} + {{#form-fields/field-wrapper style=(if style style "horizontal")}} + + + + + + + + + + + + + {{#if model.rateLimits}} + {{#each model.rateLimits as |rateLimit|}} + + + + + + + + + {{/each}} + {{else}} + + {{/if}} + +
DurationLimit ByLimit + Primary + +
+ {{input type="text" value=rateLimit.durationInUnits class="form-control input-xs rate-limit-duration-in-units"}} + + {{select-menu value=rateLimit.durationUnits action=(action (mut rateLimit.durationUnits)) options=rateLimitDurationUnitOptions inputClass="form-control input-xs rate-limit-duration-units"}} + + {{select-menu value=rateLimit.limitBy action=(action (mut rateLimit.limitBy)) options=rateLimitLimitByOptions inputClass="form-control input-xs rate-limit-limit-by"}} + +
+ {{input type="text" value=rateLimit.limit class="form-control input-xs rate-limit-limit"}} + requests +
+
+ {{one-way-radio name=uniqueSettingsId value=rateLimit.responseHeaders option=true class="rate-limit-response-headers" update=(action "primaryRateLimitChange" rateLimit)}} + + Remove +
No custom rate limits have been added yet. Click "Add Rate Limit" below to get started.
+ + {{/form-fields/field-wrapper}} + {{/if}} +{{/fields-for}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/sub-settings-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/sub-settings-form.hbs new file mode 100644 index 000000000..6bb880484 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/sub-settings-form.hbs @@ -0,0 +1,19 @@ +{{#bs-modal open=openModal submitAction=(action "submit") closedAction=(action "closed") title=modalTitle body=false footer=false backdropClose=false size="lg"}} + {{#bs-modal-body}} +
+ {{#fields-for model=bufferedModel style="vertical" as |f|}} +
+
+ {{f.select-field "httpMethod" label="HTTP Method" options=httpMethodOptions}} +
+
+ {{f.text-field "regex" label="Regex" placeholder="^/example.*param1=.+"}} +
+
+ {{/fields-for}} + {{apis/settings/common-fields model=model.settings isSubSettings=true}} + +
+ {{/bs-modal-body}} + {{bs-modal-footer closeTitle="Cancel" submitTitle="OK"}} +{{/bs-modal}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/sub-settings-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/sub-settings-table.hbs new file mode 100644 index 000000000..7a0300616 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/sub-settings-table.hbs @@ -0,0 +1,38 @@ +{{apis/sub-settings-form model=subSettingsModel collection=model.subSettings openModal=openModal}} + + + + + + + + + + + {{#if model.subSettings}} + {{#each model.subSettings as |subSettings|}} + + + + + + + {{/each}} + {{else}} + + {{/if}} + +
HTTP MethodURL Matcher
{{subSettings.httpMethod}}{{subSettings.regex}} + Edit + Remove +
No sub-URL request settings have been added yet. Click "Add URL Settings" below to get started.
+
+
+ +
+
+ {{#if isReorderable}} + + {{/if}} +
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/url-match-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/url-match-form.hbs new file mode 100644 index 000000000..87a4cb5fc --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/url-match-form.hbs @@ -0,0 +1,33 @@ +{{#bs-modal open=openModal submitAction=(action "submit") closedAction=(action "closed") title=modalTitle body=false footer=false backdropClose=false size="lg"}} + {{#bs-modal-body}} +
+ {{#fields-for model=bufferedModel style="vertical" as |f|}} +
+
+ {{f.text-field "frontendPrefix" label="Frontend Prefix" placeholder="/example/"}} +
+
+ +
rewrite to
+
+
+ {{f.text-field "backendPrefix" label="Backend Prefix" placeholder="/example/"}} +
+
+ {{/fields-for}} + + {{#unless id}} +

+ Tip: We recommend using trailing slashes when configuring these URL prefixes when possible (for example, using /wind/ instead of /wind). While not necessary, this helps prevent future overlapping conflicts (for example, if someone later wants to set up /windmill/). +

+ {{/unless}} + +

Example:

+
Incoming Frontend Request: {{exampleIncomingUrl}}
+
Outgoing Backend Request: {{exampleOutgoingUrl}}
+ + +
+ {{/bs-modal-body}} + {{bs-modal-footer closeTitle="Cancel" submitTitle="OK"}} +{{/bs-modal}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/apis/url-match-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/apis/url-match-table.hbs new file mode 100644 index 000000000..03dac6363 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/apis/url-match-table.hbs @@ -0,0 +1,41 @@ +{{apis/url-match-form model=urlMatchModel collection=model.urlMatches apiExampleIncomingUrlRoot=model.exampleIncomingUrlRoot openModal=openModal}} + +
+ + + + + + + + + + + {{#if model.urlMatches}} + {{#each model.urlMatches as |urlMatch|}} + + + + + + + {{/each}} + {{else}} + + {{/if}} + +
Frontend PrefixBackend Prefix
{{urlMatch.frontendPrefix}}{{urlMatch.backendPrefixWithDefault}} + {{t "admin.edit"}} + {{t "admin.remove"}} +
{{t "admin.api.url_matches.empty_list" add=(t "admin.api.url_matches.add")}}
+
+
+ +
+
+ {{#if isReorderable}} + + {{/if}} +
+
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/busy-blocker.hbs b/src/api-umbrella/admin-ui/app/templates/components/busy-blocker.hbs new file mode 100644 index 000000000..4c627c718 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/busy-blocker.hbs @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/config/publish-form-records.hbs b/src/api-umbrella/admin-ui/app/templates/components/config/publish-form-records.hbs new file mode 100644 index 000000000..4686c4389 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/config/publish-form-records.hbs @@ -0,0 +1,34 @@ +{{#if records}} + + + + + + + + + {{#each records as |record|}} + + + + + + + + + {{/each}} + +
Publish?Name
+ + + + + + {{record.name}}
View Config Differences
+{{else}} +
None
+{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/config/publish-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/config/publish-form.hbs new file mode 100644 index 000000000..52c4d8ca7 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/config/publish-form.hbs @@ -0,0 +1,62 @@ +{{#if hasChanges}} +
+ {{#if model.config.apis.new}} +
+ {{model.config.apis.new.length}} New API Backends + {{config/publish-form-records category="apis" records=model.config.apis.new}} +
+ {{/if}} + + {{#if model.config.website_backends.new}} +
+ {{model.config.website_backends.new.length}} New Website Backends + {{config/publish-form-records category="website_backends" records=model.config.website_backends.new}} +
+ {{/if}} + + {{#if model.config.apis.modified}} +
+ {{model.config.apis.modified.length}} Modified API Backends + {{config/publish-form-records category="apis" records=model.config.apis.modified}} +
+ {{/if}} + + {{#if model.config.website_backends.modified}} +
+ {{model.config.website_backends.modified.length}} Modified Website Backends + {{config/publish-form-records category="website_backends" records=model.config.website_backends.modified}} +
+ {{/if}} + + {{#if model.config.apis.deleted}} +
+ {{model.config.apis.deleted.length}} Deleted API Backends + {{config/publish-form-records category="apis" records=model.config.apis.deleted}} +
+ {{/if}} + + {{#if model.config.website_backends.deleted}} +
+ {{model.config.website_backends.deleted.length}} Deleted Website Backends + {{config/publish-form-records category="website_backends" records=model.config.website_backends.deleted}} +
+ {{/if}} + + + +
+ +
+
+{{else}} +
+
+
+ Published configuration is up to date
+ Recently published changes should be live within a few seconds +
+
+
+{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/error-messages.hbs b/src/api-umbrella/admin-ui/app/templates/components/error-messages.hbs new file mode 100644 index 000000000..170b82c1a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/error-messages.hbs @@ -0,0 +1,12 @@ +
+ {{#if hasErrors}} + + {{/if}} +
diff --git a/src/api-umbrella/admin-ui/app/templates/components/fields-for.hbs b/src/api-umbrella/admin-ui/app/templates/components/fields-for.hbs new file mode 100644 index 000000000..0623a8f59 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/fields-for.hbs @@ -0,0 +1,10 @@ +{{yield (hash + ace-field=(component "form-fields/ace-field" model=model style=style) + checkbox-field=(component "form-fields/checkbox-field" model=model style=style) + checkboxes-field=(component "form-fields/checkboxes-field" model=model style=style) + select-field=(component "form-fields/select-field" model=model style=style) + selectize-field=(component "form-fields/selectize-field" model=model style=style) + text-field=(component "form-fields/text-field" model=model style=style) + textarea-field=(component "form-fields/textarea-field" model=model style=style) + static-field=(component "form-fields/static-field" model=model style=style) +)}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/ace-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/ace-field.hbs new file mode 100644 index 000000000..694f2f2c4 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/ace-field.hbs @@ -0,0 +1,3 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId labelForId=aceTextInputId label=label tooltip=tooltip}} + {{one-way-textarea value=(get model fieldName) update=(action (mut (get model fieldName))) class="form-control" data-ace-mode=mode}} +{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/base-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/base-field.hbs new file mode 100644 index 000000000..889d9eead --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/base-field.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkbox-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkbox-field.hbs new file mode 100644 index 000000000..4a952de4e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkbox-field.hbs @@ -0,0 +1,5 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId}} +
+ +
+{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkboxes-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkboxes-field.hbs new file mode 100644 index 000000000..2c82d545c --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkboxes-field.hbs @@ -0,0 +1,7 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} + {{#multiselect-checkboxes options=options selection=(get model fieldName) tagName="div" valueProperty="id" as |option isSelected|}} +
+ +
+ {{/multiselect-checkboxes}} +{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/error-messages.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/error-messages.hbs new file mode 100644 index 000000000..c8b01a724 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/error-messages.hbs @@ -0,0 +1,7 @@ +{{#if hasErrors}} +
+ {{#each errorMessages as |message|}} + {{message}}
+ {{/each}} +
+{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/field-wrapper.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/field-wrapper.hbs new file mode 100644 index 000000000..23b3cf2cf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/field-wrapper.hbs @@ -0,0 +1,34 @@ +{{#if (eq style "horizontal")}} +
+
+
+ + {{help-tooltip tooltip=tooltip}} +
+
+ {{yield}} + {{form-fields/error-messages hasErrors=fieldErrorMessages errorMessages=fieldErrorMessages}} +
+
+
+{{else if (eq style "horizontal-wide-labels")}} +
+
+
+ + {{help-tooltip tooltip=tooltip}} +
+
+ {{yield}} + {{form-fields/error-messages hasErrors=fieldErrorMessages errorMessages=fieldErrorMessages}} +
+
+
+{{else}} +
+ + {{help-tooltip tooltip=tooltip}} + {{yield}} + {{form-fields/error-messages hasErrors=fieldErrorMessages errorMessages=fieldErrorMessages}} +
+{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/select-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/select-field.hbs new file mode 100644 index 000000000..ebe2b6f2d --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/select-field.hbs @@ -0,0 +1,3 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} + {{select-menu value=(get model fieldName) action=(action (mut (get model fieldName))) options=options inputId=inputId inputClass="form-control"}} +{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/selectize-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/selectize-field.hbs new file mode 100644 index 000000000..df4c2305e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/selectize-field.hbs @@ -0,0 +1,3 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId labelForId=selectizeTextInputId label=label tooltip=tooltip}} + +{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/static-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/static-field.hbs new file mode 100644 index 000000000..3901d2c9b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/static-field.hbs @@ -0,0 +1,9 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} +
+ {{#if hasBlock}} + {{yield}} + {{else}} + {{get model fieldName}} + {{/if}} +
+{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/text-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/text-field.hbs new file mode 100644 index 000000000..f1f9cdf6b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/text-field.hbs @@ -0,0 +1,3 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} + +{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/textarea-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/textarea-field.hbs new file mode 100644 index 000000000..97e59ed5e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/textarea-field.hbs @@ -0,0 +1,3 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} + +{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/help-tooltip.hbs b/src/api-umbrella/admin-ui/app/templates/components/help-tooltip.hbs new file mode 100644 index 000000000..0e7e05890 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/help-tooltip.hbs @@ -0,0 +1,3 @@ +{{#if tooltip}} + Help +{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/select-menu.hbs b/src/api-umbrella/admin-ui/app/templates/components/select-menu.hbs new file mode 100644 index 000000000..543990380 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/select-menu.hbs @@ -0,0 +1,5 @@ +{{#x-select value=value one-way=true action=action id=inputId class=inputClass}} + {{#each options as |option|}} + {{#x-option value=option.id}}{{option.name}}{{/x-option}} + {{/each}} +{{/x-select}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-breadcrumbs.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-breadcrumbs.hbs new file mode 100644 index 000000000..93127e0d8 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-breadcrumbs.hbs @@ -0,0 +1,7 @@ +{{#each breadcrumbLinks as |breadcrumb|}} + {{#if breadcrumb.prefix}} + {{#link-to "stats.drilldown" (query-params prefix=breadcrumb.prefix)}}{{breadcrumb.name}}{{/link-to}} / + {{else}} + {{breadcrumb.name}} + {{/if}} +{{/each}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-chart.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-chart.hbs new file mode 100644 index 000000000..889d9eead --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-chart.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-table.hbs new file mode 100644 index 000000000..acd08a54e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/drilldown/results-table.hbs @@ -0,0 +1,6 @@ +
+
+
+ diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-chart.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-chart.hbs new file mode 100644 index 000000000..889d9eead --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-chart.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-facet-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-facet-table.hbs new file mode 100644 index 000000000..b78c35632 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-facet-table.hbs @@ -0,0 +1,19 @@ + + + + + + + + + + + {{#each facets as |facet|}} + + + + + + {{/each}} + + diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-highlights.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-highlights.hbs new file mode 100644 index 000000000..d132c13e0 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-highlights.hbs @@ -0,0 +1,22 @@ +
+
+ {{format-number stats.total_hits format="0,0"}} + {{inflect "hit" stats.total_hits}} +
+
+ {{format-number stats.total_users format="0,0"}} + unique {{inflect "user" stats.total_users}} + + {{stats/logs/results-facet-table label="view top users" field="user_email" facets=aggregations.users queryParamValues=queryParamValues}} +
+
+ {{format-number stats.total_ips format="0,0"}} + unique ip {{inflect "address" stats.total_ips}} + + {{stats/logs/results-facet-table label="view top ips" field="request_ip" facets=aggregations.ips queryParamValues=queryParamValues}} +
+
+ {{format-number stats.average_response_time format="0,0"}} ms + average response time +
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-table.hbs new file mode 100644 index 000000000..acd08a54e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/logs/results-table.hbs @@ -0,0 +1,6 @@ +
+
+
+ diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-breadcrumbs.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-breadcrumbs.hbs new file mode 100644 index 000000000..ff97b0904 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-breadcrumbs.hbs @@ -0,0 +1,7 @@ +{{#each breadcrumbLinks as |breadcrumb|}} + {{#if breadcrumb.region}} + {{#link-to "stats.map" (query-params region=breadcrumb.region)}}{{breadcrumb.name}}{{/link-to}} / + {{else}} + {{breadcrumb.name}} + {{/if}} +{{/each}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-map.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-map.hbs new file mode 100644 index 000000000..889d9eead --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-map.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-table.hbs new file mode 100644 index 000000000..acd08a54e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/map/results-table.hbs @@ -0,0 +1,6 @@ +
+
+
+ diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/query-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/query-form.hbs new file mode 100644 index 000000000..62592db23 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/query-form.hbs @@ -0,0 +1,181 @@ +
+
+ +
+ {{#if session.data.authenticated.enable_beta_analytics}} + + {{/if}} + + {{#if enableInterval}} + {{#bs-button-group value=interval type="radio" size="xs"}} + {{#bs-button value="minute"}}{{t "admin.stats.minute"}}{{/bs-button}} + {{#bs-button value="hour"}}{{t "admin.stats.hour"}}{{/bs-button}} + {{#bs-button value="day"}}{{t "admin.stats.day"}}{{/bs-button}} + {{#bs-button value="week"}}{{t "admin.stats.week"}}{{/bs-button}} + {{#bs-button value="month"}}{{t "admin.stats.month"}}{{/bs-button}} + {{/bs-button-group}} + {{/if}} + + +
+
+
+
+ +
+
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/stats/users/results-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/stats/users/results-table.hbs new file mode 100644 index 000000000..acd08a54e --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/stats/users/results-table.hbs @@ -0,0 +1,6 @@ +
+
+
+ diff --git a/src/api-umbrella/admin-ui/app/templates/components/website-backends/index-table.hbs b/src/api-umbrella/admin-ui/app/templates/components/website-backends/index-table.hbs new file mode 100644 index 000000000..52f6a684f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/website-backends/index-table.hbs @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/api-umbrella/admin-ui/app/templates/components/website-backends/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/website-backends/record-form.hbs new file mode 100644 index 000000000..af7970245 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/website-backends/record-form.hbs @@ -0,0 +1,29 @@ +{{error-messages model=model}} + +
+ {{#fields-for model=model style="horizontal" as |f|}} +
+ {{f.text-field "frontendHost" label="Frontend Host" placeholder="example.com"}} + {{f.select-field "backendProtocol" label=(t "mongoid.attributes.api.backend_protocol") options=backendProtocolOptions}} + {{f.text-field "serverHost" label="Backend Server" placeholder="example.github.io"}} + {{f.text-field "serverPort" label="Backend Port"}} +
+ +
+
+ +
+
+ {{#if model.id}} + Created: {{format-date model.createdAt}} by {{model.creator.username}}
+ Last Updated: {{format-date model.updatedAt}} by {{model.updater.username}}
+ {{/if}} +
+
+ {{#if model.id}} + + {{/if}} + {{/fields-for}} +
diff --git a/src/api-umbrella/admin-ui/app/templates/config/publish.hbs b/src/api-umbrella/admin-ui/app/templates/config/publish.hbs new file mode 100644 index 000000000..03738509f --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/config/publish.hbs @@ -0,0 +1,2 @@ +

Publish Configuration Changes

+{{config/publish-form model=model}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis.hbs b/src/api-umbrella/admin-ui/app/templates/login.hbs similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis.hbs rename to src/api-umbrella/admin-ui/app/templates/login.hbs diff --git a/src/api-umbrella/admin-ui/app/templates/not-found.hbs b/src/api-umbrella/admin-ui/app/templates/not-found.hbs new file mode 100644 index 000000000..cbbebf43d --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/not-found.hbs @@ -0,0 +1 @@ +

Page Not Found

diff --git a/src/api-umbrella/admin-ui/app/templates/stats/drilldown.hbs b/src/api-umbrella/admin-ui/app/templates/stats/drilldown.hbs new file mode 100644 index 000000000..11a16cce4 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/stats/drilldown.hbs @@ -0,0 +1,4 @@ +{{stats/query-form enableInterval=true start_at=start_at end_at=end_at query=query search=search interval=interval beta_analytics=beta_analytics}} +{{stats/drilldown/results-chart hitsOverTime=model.hits_over_time}} +{{stats/drilldown/results-breadcrumbs breadcrumbs=model.breadcrumbs}} +{{stats/drilldown/results-table results=model.results queryParamValues=queryParamValues allQueryParamValues=allQueryParamValues}} diff --git a/src/api-umbrella/admin-ui/app/templates/stats/logs.hbs b/src/api-umbrella/admin-ui/app/templates/stats/logs.hbs new file mode 100644 index 000000000..5bfd549ac --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/stats/logs.hbs @@ -0,0 +1,4 @@ +{{stats/query-form enableInterval=true start_at=start_at end_at=end_at query=query search=search interval=interval beta_analytics=beta_analytics}} +{{stats/logs/results-chart hitsOverTime=model.hits_over_time}} +{{stats/logs/results-highlights stats=model.stats aggregations=model.aggregations queryParamValues=queryParamValues}} +{{stats/logs/results-table queryParamValues=queryParamValues allQueryParamValues=allQueryParamValues}} diff --git a/src/api-umbrella/admin-ui/app/templates/stats/map.hbs b/src/api-umbrella/admin-ui/app/templates/stats/map.hbs new file mode 100644 index 000000000..6aad7c3c9 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/stats/map.hbs @@ -0,0 +1,6 @@ +{{stats/query-form enableInterval=false start_at=start_at end_at=end_at query=query search=search region=region beta_analytics=beta_analytics}} +
+ {{stats/map/results-breadcrumbs breadcrumbs=model.map_breadcrumbs}} + {{stats/map/results-map regions=model.map_regions regionField=model.region_field queryParamValues=queryParamValues allQueryParamValues=allQueryParamValues}} +
+{{stats/map/results-table regions=model.regions regionField=model.region_field queryParamValues=queryParamValues allQueryParamValues=allQueryParamValues}} diff --git a/src/api-umbrella/admin-ui/app/templates/stats/users.hbs b/src/api-umbrella/admin-ui/app/templates/stats/users.hbs new file mode 100644 index 000000000..67af79a11 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/stats/users.hbs @@ -0,0 +1,2 @@ +{{stats/query-form enableInterval=false start_at=start_at end_at=end_at query=query search=search beta_analytics=beta_analytics}} +{{stats/users/results-table queryParamValues=queryParamValues allQueryParamValues=allQueryParamValues}} diff --git a/src/api-umbrella/admin-ui/app/templates/website-backends/edit.hbs b/src/api-umbrella/admin-ui/app/templates/website-backends/edit.hbs new file mode 100644 index 000000000..82932e2cb --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/website-backends/edit.hbs @@ -0,0 +1,2 @@ +

Edit Website Backend

+{{website-backends/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/templates/website-backends/index.hbs b/src/api-umbrella/admin-ui/app/templates/website-backends/index.hbs new file mode 100644 index 000000000..6a5ba376a --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/website-backends/index.hbs @@ -0,0 +1,7 @@ +

Website Backends

+ +
+ {{#link-to "website_backends.new" class="btn btn-primary"}} Add Website Backend{{/link-to}} +
+ +{{website-backends/index-table}} diff --git a/src/api-umbrella/admin-ui/app/templates/website-backends/new.hbs b/src/api-umbrella/admin-ui/app/templates/website-backends/new.hbs new file mode 100644 index 000000000..7b22fc744 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/website-backends/new.hbs @@ -0,0 +1,2 @@ +

Add Website Backend

+{{website-backends/record-form model=model}} diff --git a/src/api-umbrella/admin-ui/app/utils/data-tables-helpers.js b/src/api-umbrella/admin-ui/app/utils/data-tables-helpers.js new file mode 100644 index 000000000..2e0fcfde8 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/utils/data-tables-helpers.js @@ -0,0 +1,29 @@ +export default { + renderEscaped(value, type) { + if(type === 'display' && value) { + return _.escape(value); + } + + return value; + }, + + renderListEscaped(value, type) { + if(type === 'display' && value) { + if(_.isArray(value)) { + return _.map(value, function(v) { return _.escape(v); }).join('
'); + } else { + return _.escape(value); + } + } + + return value; + }, + + renderTime(value, type) { + if(type === 'display' && value && value !== '-') { + return moment(value).format('YYYY-MM-DD HH:mm:ss'); + } + + return value; + }, +}; diff --git a/src/api-umbrella/admin-ui/bower.json b/src/api-umbrella/admin-ui/bower.json new file mode 100644 index 000000000..6fae76714 --- /dev/null +++ b/src/api-umbrella/admin-ui/bower.json @@ -0,0 +1,32 @@ +{ + "name": "api-umbrella-admin-ui", + "dependencies": { + "ace-builds": "1.2.6", + "bootbox": "4.4.0", + "bootstrap": "3.3.7", + "bootstrap-daterangepicker": "2.1.24", + "bootstrap-sass": "3.3.7", + "bootswatch": "3.3.7", + "datatables": "1.10.12", + "ember": "2.8.3", + "ember-cli-shims": "0.1.3", + "ember-qunit-notifications": "0.1.0", + "font-awesome": "4.7.0", + "inflection": "1.10.0", + "jQuery-QueryBuilder": "2.4.0", + "jquery-bbq-deparam": "1.2.1", + "jquery-ui": "1.12.1", + "jquery.scrollTo": "2.1.2", + "jsdiff": "2.2.2", + "jstz-detect": "1.0.5", + "lodash": "4.17.2", + "marked": "0.3.6", + "moment": "2.17.0", + "numeral": "1.5.6", + "pnotify": "3.0.0", + "polyglot": "2.2.1", + "qtip2": "2.2.1", + "selectize": "0.12.4", + "tbasse-jquery-truncate": "#2247c21d70591a516d9a484aaba83db0a908e32f" + } +} diff --git a/src/api-umbrella/admin-ui/config/environment.js b/src/api-umbrella/admin-ui/config/environment.js new file mode 100644 index 000000000..13256d049 --- /dev/null +++ b/src/api-umbrella/admin-ui/config/environment.js @@ -0,0 +1,43 @@ +/* eslint-env node */ + +module.exports = function(environment) { + let ENV = { + modulePrefix: 'api-umbrella-admin-ui', + environment: environment, + baseURL: '/admin/', + locationType: 'hash', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. 'with-controller': true + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if(environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if(environment === 'test') { + // Testem prefers this... + ENV.baseURL = '/'; + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + } + + return ENV; +}; diff --git a/src/api-umbrella/admin-ui/ember-cli-build.js b/src/api-umbrella/admin-ui/ember-cli-build.js new file mode 100644 index 000000000..270232608 --- /dev/null +++ b/src/api-umbrella/admin-ui/ember-cli-build.js @@ -0,0 +1,83 @@ +/* eslint-env node */ +/* global require, module */ +let EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function(defaults) { + let app = new EmberApp(defaults, { + sassOptions: { + // The Sass number precision must be increased to 8 for Bootstrap, or + // else certain things don't line up: + // https://github.com/twbs/bootstrap-sass#sass-number-precision + precision: 8, + }, + }); + + // Use `app.import` to add additional libraries to the generated + // output files. + // + // If you need to use different assets in different + // environments, specify an object as the first parameter. That + // object's keys should be the environment name and the values + // should be the asset to use in that environment. + // + // If the library that you are including contains AMD or ES6 + // modules that you would like to import into your application + // please specify an object with the list of modules as keys + // along with the exports of each module as its value. + + // Prepend Ace before Ember, or else the "define" method from Ember's + // loader.js conflicts with Ace after these changes in Ace v1.2.4: + // https://github.com/ajaxorg/ace/pull/2914 + app.import('bower_components/ace-builds/src-noconflict/mode-json.js', { prepend: true }); + app.import('bower_components/ace-builds/src-noconflict/mode-xml.js', { prepend: true }); + app.import('bower_components/ace-builds/src-noconflict/mode-yaml.js', { prepend: true }); + app.import('bower_components/ace-builds/src-noconflict/ace.js', { prepend: true }); + + app.import('bower_components/bootbox/bootbox.js'); + app.import('bower_components/bootstrap-sass/assets/javascripts/bootstrap.js'); + app.import('bower_components/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.eot', { destDir: 'fonts/bootstrap' }); + app.import('bower_components/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.svg', { destDir: 'fonts/bootstrap' }); + app.import('bower_components/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf', { destDir: 'fonts/bootstrap' }); + app.import('bower_components/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff', { destDir: 'fonts/bootstrap' }); + app.import('bower_components/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2', { destDir: 'fonts/bootstrap' }); + app.import('bower_components/datatables/media/css/dataTables.bootstrap.css'); + app.import('bower_components/datatables/media/js/jquery.dataTables.js'); + app.import('bower_components/datatables/media/js/dataTables.bootstrap.js'); + app.import('bower_components/font-awesome/fonts/fontawesome-webfont.eot', { destDir: 'fonts' }); + app.import('bower_components/font-awesome/fonts/fontawesome-webfont.svg', { destDir: 'fonts' }); + app.import('bower_components/font-awesome/fonts/fontawesome-webfont.ttf', { destDir: 'fonts' }); + app.import('bower_components/font-awesome/fonts/fontawesome-webfont.woff', { destDir: 'fonts' }); + app.import('bower_components/font-awesome/fonts/fontawesome-webfont.woff2', { destDir: 'fonts' }); + app.import('bower_components/inflection/lib/inflection.js'); + app.import('bower_components/jQuery-QueryBuilder/dist/js/query-builder.standalone.js'); + app.import('bower_components/jQuery-QueryBuilder/dist/js/query-builder.standalone.js'); + app.import('bower_components/jquery-bbq-deparam/jquery-deparam.js'); + app.import('bower_components/jquery.scrollTo/jquery.scrollTo.js'); + app.import('bower_components/jsdiff/diff.js'); + app.import('bower_components/jstz-detect/jstz.js'); + app.import('bower_components/lodash/dist/lodash.js'); + app.import('bower_components/marked/lib/marked.js'); + app.import('bower_components/moment/moment.js'); + app.import('bower_components/pnotify/dist/pnotify.css'); + app.import('bower_components/pnotify/dist/pnotify.buttons.css'); + app.import('bower_components/pnotify/dist/pnotify.mobile.css'); + app.import('bower_components/pnotify/dist/pnotify.js'); + app.import('bower_components/pnotify/dist/pnotify.buttons.js'); + app.import('bower_components/pnotify/dist/pnotify.mobile.js'); + app.import('bower_components/qtip2/jquery.qtip.css'); + app.import('bower_components/qtip2/jquery.qtip.js'); + app.import('bower_components/selectize/dist/css/selectize.default.css'); + app.import('bower_components/selectize/dist/js/standalone/selectize.js'); + app.import('bower_components/jquery-ui/ui/version.js'); + app.import('bower_components/jquery-ui/ui/data.js'); + app.import('bower_components/jquery-ui/ui/ie.js'); + app.import('bower_components/jquery-ui/ui/scroll-parent.js'); + app.import('bower_components/jquery-ui/ui/widget.js'); + app.import('bower_components/jquery-ui/ui/widgets/mouse.js'); + app.import('bower_components/jquery-ui/ui/widgets/sortable.js'); + app.import('bower_components/tbasse-jquery-truncate/jquery.truncate.js'); + + app.import('bower_components/bootstrap-daterangepicker/daterangepicker.js'); + + return app.toTree(); +}; diff --git a/src/api-umbrella/admin-ui/lib/.jshintrc b/src/api-umbrella/admin-ui/lib/.jshintrc new file mode 100644 index 000000000..839c191fa --- /dev/null +++ b/src/api-umbrella/admin-ui/lib/.jshintrc @@ -0,0 +1,4 @@ +{ + "node": true, + "browser": false +} diff --git a/src/api-umbrella/admin-ui/lib/inject-live-reload/index.js b/src/api-umbrella/admin-ui/lib/inject-live-reload/index.js new file mode 100644 index 000000000..083af1368 --- /dev/null +++ b/src/api-umbrella/admin-ui/lib/inject-live-reload/index.js @@ -0,0 +1,49 @@ +/*jshint node:true*/ + +// Copy of ember-cli-inject-live-reload, but customized to allow for proxying +// behind HTTPS. +// +// Copied as of this commit: +// https://github.com/rwjblue/ember-cli-inject-live-reload/blob/bf144a1c417cd6700f177ee92424dec7b7f53b73/index.js +module.exports = { + name: 'live-reload', + + contentFor: function(type) { + var liveReloadPort = process.env.EMBER_CLI_INJECT_LIVE_RELOAD_PORT; + var baseURL = process.env.EMBER_CLI_INJECT_LIVE_RELOAD_BASEURL; + + if (liveReloadPort && type === 'head') { + return ''; + } + }, + + dynamicScript: function(request) { + var liveReloadPort = process.env.EMBER_CLI_INJECT_LIVE_RELOAD_PORT; + + return "(function() {\n " + + "var src = (location.protocol || 'http:') + '//' + (location.hostname || 'localhost') + ':" + liveReloadPort + "/livereload.js?snipver=1&port=" + liveReloadPort + "';\n " + + "var script = document.createElement('script');\n " + + "script.type = 'text/javascript';\n " + + "script.src = src;\n " + + "document.getElementsByTagName('head')[0].appendChild(script);\n" + + "}());"; + }, + + serverMiddleware: function(config) { + var self = this; + var app = config.app; + var options = config.options; + + if (options.liveReload !== true) { return; } + + if(!process.env.EMBER_CLI_INJECT_LIVE_RELOAD_PORT) { + process.env.EMBER_CLI_INJECT_LIVE_RELOAD_PORT = options.liveReloadPort; + } + process.env.EMBER_CLI_INJECT_LIVE_RELOAD_BASEURL = options.liveReloadBaseUrl || options.baseURL; + + app.use(options.baseURL + 'ember-cli-live-reload.js', function(request, response, next) { + response.contentType('text/javascript'); + response.send(self.dynamicScript()); + }); + } +}; diff --git a/src/api-umbrella/admin-ui/lib/inject-live-reload/package.json b/src/api-umbrella/admin-ui/lib/inject-live-reload/package.json new file mode 100644 index 000000000..a2e4e965f --- /dev/null +++ b/src/api-umbrella/admin-ui/lib/inject-live-reload/package.json @@ -0,0 +1,9 @@ +{ + "name": "inject-live-reload", + "keywords": [ + "ember-addon" + ], + "ember-addon": { + "before": "serve-files-middleware" + } +} diff --git a/src/api-umbrella/admin-ui/package.json b/src/api-umbrella/admin-ui/package.json new file mode 100644 index 000000000..ed026b80f --- /dev/null +++ b/src/api-umbrella/admin-ui/package.json @@ -0,0 +1,57 @@ +{ + "name": "api-umbrella-admin-ui", + "version": "0.0.0", + "description": "Small description for api-umbrella-admin-ui goes here", + "private": true, + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build", + "start": "ember server", + "test": "ember test" + }, + "repository": "", + "engines": { + "node": ">= 4.6.0" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "bower": "~1.8.0", + "broccoli-asset-rev": "^2.4.2", + "ember-bootstrap": "~0.11.2", + "ember-buffered-proxy": "~0.6.0", + "ember-busy-blocker": "~0.1.0", + "ember-cli": "~2.8.0", + "ember-cli-babel": "^5.1.6", + "ember-cli-dependency-checker": "^1.2.0", + "ember-cli-eslint": "~3.0.0", + "ember-cli-format-number": "~2.0.0", + "ember-cli-htmlbars": "^1.0.3", + "ember-cli-htmlbars-inline-precompile": "^0.3.1", + "ember-cli-qunit": "^1.4.0", + "ember-cli-sass": "~5.6.0", + "ember-cli-sri": "^2.1.0", + "ember-cli-uglify": "^1.2.0", + "ember-cp-validations": "~3.1.2", + "ember-data": "~2.8.0", + "ember-export-application-global": "^1.0.5", + "ember-load": "~0.0.11", + "ember-load-initializers": "^0.5.1", + "ember-multiselect-checkboxes": "~0.10.1", + "ember-onbeforeunload": "~1.1.0", + "ember-one-way-controls": "~1.1.2", + "ember-resolver": "^2.0.3", + "ember-simple-auth": "~1.1.0", + "ember-truth-helpers": "~1.2.0", + "emberx-select": "~2.2.2", + "loader.js": "^4.0.1" + }, + "ember-addon": { + "paths": [ + "lib/inject-live-reload" + ] + } +} diff --git a/src/api-umbrella/admin-ui/testem.js b/src/api-umbrella/admin-ui/testem.js new file mode 100644 index 000000000..dd980cb00 --- /dev/null +++ b/src/api-umbrella/admin-ui/testem.js @@ -0,0 +1,13 @@ +/* eslint-env node */ +module.exports = { + "framework": "qunit", + "test_page": "tests/index.html?hidepassed", + "disable_watching": true, + "launch_in_ci": [ + "PhantomJS", + ], + "launch_in_dev": [ + "PhantomJS", + "Chrome", + ], +}; diff --git a/src/api-umbrella/admin-ui/testem.json b/src/api-umbrella/admin-ui/testem.json new file mode 100644 index 000000000..0f35392cf --- /dev/null +++ b/src/api-umbrella/admin-ui/testem.json @@ -0,0 +1,12 @@ +{ + "framework": "qunit", + "test_page": "tests/index.html?hidepassed", + "disable_watching": true, + "launch_in_ci": [ + "PhantomJS" + ], + "launch_in_dev": [ + "PhantomJS", + "Chrome" + ] +} diff --git a/src/api-umbrella/admin-ui/tests/.eslintrc.js b/src/api-umbrella/admin-ui/tests/.eslintrc.js new file mode 100644 index 000000000..38b342806 --- /dev/null +++ b/src/api-umbrella/admin-ui/tests/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + env: { + embertest: true + }, + rules: { + 'comma-dangle': 'off', + 'object-shorthand': 'off', + 'keyword-spacing': 'off', + }, +}; diff --git a/src/api-umbrella/admin-ui/tests/helpers/destroy-app.js b/src/api-umbrella/admin-ui/tests/helpers/destroy-app.js new file mode 100644 index 000000000..c3d4d1abb --- /dev/null +++ b/src/api-umbrella/admin-ui/tests/helpers/destroy-app.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +export default function destroyApp(application) { + Ember.run(application, 'destroy'); +} diff --git a/src/api-umbrella/admin-ui/tests/helpers/module-for-acceptance.js b/src/api-umbrella/admin-ui/tests/helpers/module-for-acceptance.js new file mode 100644 index 000000000..76996fd04 --- /dev/null +++ b/src/api-umbrella/admin-ui/tests/helpers/module-for-acceptance.js @@ -0,0 +1,23 @@ +import { module } from 'qunit'; +import Ember from 'ember'; +import startApp from '../helpers/start-app'; +import destroyApp from '../helpers/destroy-app'; + +const { RSVP: { Promise } } = Ember; + +export default function(name, options = {}) { + module(name, { + beforeEach() { + this.application = startApp(); + + if (options.beforeEach) { + return options.beforeEach.apply(this, arguments); + } + }, + + afterEach() { + let afterEach = options.afterEach && options.afterEach.apply(this, arguments); + return Promise.resolve(afterEach).then(() => destroyApp(this.application)); + } + }); +} diff --git a/src/api-umbrella/admin-ui/tests/helpers/resolver.js b/src/api-umbrella/admin-ui/tests/helpers/resolver.js new file mode 100644 index 000000000..b208d38d0 --- /dev/null +++ b/src/api-umbrella/admin-ui/tests/helpers/resolver.js @@ -0,0 +1,11 @@ +import Resolver from '../../resolver'; +import config from '../../config/environment'; + +const resolver = Resolver.create(); + +resolver.namespace = { + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix +}; + +export default resolver; diff --git a/src/api-umbrella/admin-ui/tests/helpers/start-app.js b/src/api-umbrella/admin-ui/tests/helpers/start-app.js new file mode 100644 index 000000000..e098f1d5b --- /dev/null +++ b/src/api-umbrella/admin-ui/tests/helpers/start-app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Application from '../../app'; +import config from '../../config/environment'; + +export default function startApp(attrs) { + let application; + + let attributes = Ember.merge({}, config.APP); + attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; + + Ember.run(() => { + application = Application.create(attributes); + application.setupForTesting(); + application.injectTestHelpers(); + }); + + return application; +} diff --git a/src/api-umbrella/admin-ui/tests/index.html b/src/api-umbrella/admin-ui/tests/index.html new file mode 100644 index 000000000..94a336c48 --- /dev/null +++ b/src/api-umbrella/admin-ui/tests/index.html @@ -0,0 +1,34 @@ + + + + + + ApiUmbrellaAdmin Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + + + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/test/log/.gitkeep b/src/api-umbrella/admin-ui/tests/integration/.gitkeep similarity index 100% rename from test/log/.gitkeep rename to src/api-umbrella/admin-ui/tests/integration/.gitkeep diff --git a/src/api-umbrella/admin-ui/tests/test-helper.js b/src/api-umbrella/admin-ui/tests/test-helper.js new file mode 100644 index 000000000..35461bf4c --- /dev/null +++ b/src/api-umbrella/admin-ui/tests/test-helper.js @@ -0,0 +1,8 @@ +import resolver from './helpers/resolver'; +import registerSelectHelper from './helpers/register-select-helper'; +registerSelectHelper(); +import { + setResolver +} from 'ember-qunit'; + +setResolver(resolver); diff --git a/test/tmp/.gitkeep b/src/api-umbrella/admin-ui/tests/unit/.gitkeep similarity index 100% rename from test/tmp/.gitkeep rename to src/api-umbrella/admin-ui/tests/unit/.gitkeep diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/index_route.js b/src/api-umbrella/admin-ui/vendor/.gitkeep similarity index 100% rename from src/api-umbrella/web-app/app/assets/javascripts/admin/routes/index_route.js rename to src/api-umbrella/admin-ui/vendor/.gitkeep diff --git a/src/api-umbrella/admin-ui/yarn.lock b/src/api-umbrella/admin-ui/yarn.lock new file mode 100644 index 000000000..b7ba2f93a --- /dev/null +++ b/src/api-umbrella/admin-ui/yarn.lock @@ -0,0 +1,5545 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +abbrev@1, abbrev@~1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + +accepts@1.3.3, accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4, acorn@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.3.tgz#1a3e850b428e73ba6b09d1cc527f5aaad4d03ef1" + +after@0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.1.tgz#ab5d4fb883f596816d3515f8f791c0af486dd627" + +ajv-keywords@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.2.0.tgz#676c4f087bfe1e8b12dca6fda2f3c74f417b099c" + +ajv@^4.7.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.9.1.tgz#08e1b0a5fddc8b844d28ca7b03510e78812ee3a0" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alter@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/alter/-/alter-0.2.0.tgz#c7588808617572034aae62480af26b1d4d1cb3cd" + dependencies: + stable "~0.1.3" + +amd-name-resolver@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/amd-name-resolver/-/amd-name-resolver-0.0.5.tgz#76962dac876ed3311b05d29c6a58c14e1ef3304b" + dependencies: + ensure-posix-path "^1.0.1" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-regex@*, ansi-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + +ansi-regex@^0.2.0, ansi-regex@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" + +ansi-styles@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" + +ansi-styles@^2.1.0, ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" + +ansi@^0.3.0, ansi@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" + +ansicolors@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" + +ansicolors@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + +ansistyles@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" + +aproba@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.0.4.tgz#2713680775e7614c8ba186c065d4e2e52d1072c0" + +archy@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + +are-we-there-yet@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.0 || ^1.1.13" + +argparse@^1.0.7, argparse@~1.0.2: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-index@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" + dependencies: + debug "^2.2.0" + es6-symbol "^3.0.2" + +array-to-error@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-to-error/-/array-to-error-1.1.1.tgz#d68812926d14097a205579a667eeaf1856a44c07" + dependencies: + array-to-sentence "^1.1.0" + +array-to-sentence@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-to-sentence/-/array-to-sentence-1.1.0.tgz#c804956dafa53232495b205a9452753a258d39fc" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arraybuffer.slice@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asap@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +ast-traverse@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6" + +ast-types@0.8.12: + version "0.8.12" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.12.tgz#a0d90e4351bb887716c83fd637ebf818af4adfcc" + +ast-types@0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.15.tgz#8eef0827f04dff0ec8857ba925abe3fea6194e52" + +ast-types@0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.2.tgz#2cc19979d15c655108bf565323b8e7ee38751f6b" + +async-disk-cache@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-1.0.9.tgz#23bafb823184f463407e474e8d5f87899f72ca63" + dependencies: + debug "^2.1.3" + istextorbinary "2.1.0" + mkdirp "^0.5.0" + rimraf "^2.5.3" + rsvp "^3.0.18" + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + +async-some@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/async-some/-/async-some-1.0.2.tgz#4d8a81620d5958791b5b98f802d3207776e95509" + dependencies: + dezalgo "^1.0.2" + +async@^1.4.0, async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.0.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4" + dependencies: + lodash "^4.14.0" + +async@~0.2.6, async@~0.2.9: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" + +babel-code-frame@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.16.0.tgz#f90e60da0862909d3ce098733b5d3987c97cb8de" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^2.0.0" + +babel-core@^5.0.0: + version "5.8.38" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-5.8.38.tgz#1fcaee79d7e61b750b00b8e54f6dfc9d0af86558" + dependencies: + babel-plugin-constant-folding "^1.0.1" + babel-plugin-dead-code-elimination "^1.0.2" + babel-plugin-eval "^1.0.1" + babel-plugin-inline-environment-variables "^1.0.1" + babel-plugin-jscript "^1.0.4" + babel-plugin-member-expression-literals "^1.0.1" + babel-plugin-property-literals "^1.0.1" + babel-plugin-proto-to-assign "^1.0.3" + babel-plugin-react-constant-elements "^1.0.3" + babel-plugin-react-display-name "^1.0.3" + babel-plugin-remove-console "^1.0.1" + babel-plugin-remove-debugger "^1.0.1" + babel-plugin-runtime "^1.0.7" + babel-plugin-undeclared-variables-check "^1.0.2" + babel-plugin-undefined-to-void "^1.1.6" + babylon "^5.8.38" + bluebird "^2.9.33" + chalk "^1.0.0" + convert-source-map "^1.1.0" + core-js "^1.0.0" + debug "^2.1.1" + detect-indent "^3.0.0" + esutils "^2.0.0" + fs-readdir-recursive "^0.1.0" + globals "^6.4.0" + home-or-tmp "^1.0.0" + is-integer "^1.0.4" + js-tokens "1.0.1" + json5 "^0.4.0" + lodash "^3.10.0" + minimatch "^2.0.3" + output-file-sync "^1.1.0" + path-exists "^1.0.0" + path-is-absolute "^1.0.0" + private "^0.1.6" + regenerator "0.8.40" + regexpu "^1.3.0" + repeating "^1.1.2" + resolve "^1.1.6" + shebang-regex "^1.0.0" + slash "^1.0.0" + source-map "^0.5.0" + source-map-support "^0.2.10" + to-fast-properties "^1.0.0" + trim-right "^1.0.0" + try-resolve "^1.0.0" + +babel-plugin-constant-folding@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-constant-folding/-/babel-plugin-constant-folding-1.0.1.tgz#8361d364c98e449c3692bdba51eff0844290aa8e" + +babel-plugin-dead-code-elimination@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-dead-code-elimination/-/babel-plugin-dead-code-elimination-1.0.2.tgz#5f7c451274dcd7cccdbfbb3e0b85dd28121f0f65" + +babel-plugin-eval@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-eval/-/babel-plugin-eval-1.0.1.tgz#a2faed25ce6be69ade4bfec263f70169195950da" + +babel-plugin-feature-flags@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/babel-plugin-feature-flags/-/babel-plugin-feature-flags-0.2.3.tgz#81d81ed77bda2014098fa8243abcf03a551cbd4d" + dependencies: + json-stable-stringify "^1.0.1" + +babel-plugin-filter-imports@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/babel-plugin-filter-imports/-/babel-plugin-filter-imports-0.2.1.tgz#784f96a892f2f7ed2ccf0955688bd8916cd2e212" + dependencies: + json-stable-stringify "^1.0.1" + +babel-plugin-htmlbars-inline-precompile@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-0.1.0.tgz#b784723bd1f108796b56faf9f1c05eb5ca442983" + +babel-plugin-inline-environment-variables@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-inline-environment-variables/-/babel-plugin-inline-environment-variables-1.0.1.tgz#1f58ce91207ad6a826a8bf645fafe68ff5fe3ffe" + +babel-plugin-jscript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/babel-plugin-jscript/-/babel-plugin-jscript-1.0.4.tgz#8f342c38276e87a47d5fa0a8bd3d5eb6ccad8fcc" + +babel-plugin-member-expression-literals@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-member-expression-literals/-/babel-plugin-member-expression-literals-1.0.1.tgz#cc5edb0faa8dc927170e74d6d1c02440021624d3" + +babel-plugin-property-literals@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-property-literals/-/babel-plugin-property-literals-1.0.1.tgz#0252301900192980b1c118efea48ce93aab83336" + +babel-plugin-proto-to-assign@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/babel-plugin-proto-to-assign/-/babel-plugin-proto-to-assign-1.0.4.tgz#c49e7afd02f577bc4da05ea2df002250cf7cd123" + dependencies: + lodash "^3.9.3" + +babel-plugin-react-constant-elements@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-react-constant-elements/-/babel-plugin-react-constant-elements-1.0.3.tgz#946736e8378429cbc349dcff62f51c143b34e35a" + +babel-plugin-react-display-name@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-react-display-name/-/babel-plugin-react-display-name-1.0.3.tgz#754fe38926e8424a4e7b15ab6ea6139dee0514fc" + +babel-plugin-remove-console@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-remove-console/-/babel-plugin-remove-console-1.0.1.tgz#d8f24556c3a05005d42aaaafd27787f53ff013a7" + +babel-plugin-remove-debugger@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-remove-debugger/-/babel-plugin-remove-debugger-1.0.1.tgz#fd2ea3cd61a428ad1f3b9c89882ff4293e8c14c7" + +babel-plugin-runtime@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/babel-plugin-runtime/-/babel-plugin-runtime-1.0.7.tgz#bf7c7d966dd56ecd5c17fa1cb253c9acb7e54aaf" + +babel-plugin-undeclared-variables-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-undeclared-variables-check/-/babel-plugin-undeclared-variables-check-1.0.2.tgz#5cf1aa539d813ff64e99641290af620965f65dee" + dependencies: + leven "^1.0.2" + +babel-plugin-undefined-to-void@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/babel-plugin-undefined-to-void/-/babel-plugin-undefined-to-void-1.1.6.tgz#7f578ef8b78dfae6003385d8417a61eda06e2f81" + +babylon@^5.8.38: + version "5.8.38" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd" + +backbone@^1.1.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.3.3.tgz#4cc80ea7cb1631ac474889ce40f2f8bc683b2999" + dependencies: + underscore ">=1.8.3" + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + +base64id@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-0.1.0.tgz#02ce0fdeee0cef4f40080e1e73e834f0b1bfce3f" + +basic-auth@~1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.0.4.tgz#030935b01de7c9b94a824b29f3fccb750d3a5290" + +bcrypt-pbkdf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" + dependencies: + tweetnacl "^0.14.3" + +benchmark@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-1.0.0.tgz#2f1e2fa4c359f11122aa183082218e957e390c73" + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + dependencies: + callsite "1.0.0" + +"binaryextensions@1 || 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.0.0.tgz#e597d1a7a6a3558a2d1c7241a16c99965e6aa40f" + +bl@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.1.2.tgz#fdca871a99713aa00d19e3bbba41c44787a65398" + dependencies: + readable-stream "~2.0.5" + +blank-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/blank-object/-/blank-object-1.0.2.tgz#f990793fbe9a8c8dd013fb3219420bec81d5f4b9" + +blob@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" + +block-stream@*, block-stream@0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^2.9.33: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" + +bluebird@^3.1.1, bluebird@^3.4.6: + version "3.4.6" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" + +body-parser@~1.14.0: + version "1.14.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.14.2.tgz#1015cb1fe2c443858259581db53332f8d0cf50f9" + dependencies: + bytes "2.2.0" + content-type "~1.0.1" + debug "~2.2.0" + depd "~1.1.0" + http-errors "~1.3.1" + iconv-lite "0.4.13" + on-finished "~2.3.0" + qs "5.2.0" + raw-body "~2.1.5" + type-is "~1.6.10" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +bower-config@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.0.tgz#16c38c1135f8071c19f25938d61b0d8cbf18d3f1" + dependencies: + graceful-fs "^4.1.3" + mout "^1.0.0" + optimist "^0.6.1" + osenv "^0.1.3" + untildify "^2.1.0" + +bower-endpoint-parser@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/bower-endpoint-parser/-/bower-endpoint-parser-0.2.2.tgz#00b565adbfab6f2d35addde977e97962acbcb3f6" + +bower@^1.3.12, bower@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.0.tgz#55dbebef0ad9155382d9e9d3e497c1372345b44a" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +breakable@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/breakable/-/breakable-1.0.0.tgz#784a797915a38ead27bad456b5572cb4bbaa78c1" + +broccoli-asset-rev@^2.4.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/broccoli-asset-rev/-/broccoli-asset-rev-2.5.0.tgz#f5f66eac962bf9f086286921f0eaeaab6d00d819" + dependencies: + broccoli-asset-rewrite "^1.1.0" + broccoli-filter "^1.2.2" + json-stable-stringify "^1.0.0" + matcher-collection "^1.0.1" + rsvp "^3.0.6" + +broccoli-asset-rewrite@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-asset-rewrite/-/broccoli-asset-rewrite-1.1.0.tgz#77a5da56157aa318c59113245e8bafb4617f8830" + dependencies: + broccoli-filter "^1.2.3" + +broccoli-babel-transpiler@^5.4.5, broccoli-babel-transpiler@^5.5.0, broccoli-babel-transpiler@^5.6.0: + version "5.6.1" + resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-5.6.1.tgz#97184dcb140b40aa57f3ff38330afccc675d0a3c" + dependencies: + babel-core "^5.0.0" + broccoli-funnel "^1.0.0" + broccoli-merge-trees "^1.0.0" + broccoli-persistent-filter "^1.0.1" + clone "^0.2.0" + hash-for-dep "^1.0.2" + json-stable-stringify "^1.0.0" + +broccoli-caching-writer@^2.0.4, broccoli-caching-writer@^2.2.0, broccoli-caching-writer@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/broccoli-caching-writer/-/broccoli-caching-writer-2.3.1.tgz#b93cf58f9264f003075868db05774f4e7f25bd07" + dependencies: + broccoli-kitchen-sink-helpers "^0.2.5" + broccoli-plugin "1.1.0" + debug "^2.1.1" + rimraf "^2.2.8" + rsvp "^3.0.17" + walk-sync "^0.2.5" + +broccoli-caching-writer@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broccoli-caching-writer/-/broccoli-caching-writer-3.0.3.tgz#0bd2c96a9738d6a6ab590f07ba35c5157d7db476" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^1.2.1" + debug "^2.1.1" + rimraf "^2.2.8" + rsvp "^3.0.17" + walk-sync "^0.3.0" + +broccoli-clean-css@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-clean-css/-/broccoli-clean-css-1.1.0.tgz#9db143d9af7e0ae79c26e3ac5a9bb2d720ea19fa" + dependencies: + broccoli-persistent-filter "^1.1.6" + clean-css-promise "^0.1.0" + inline-source-map-comment "^1.0.5" + json-stable-stringify "^1.0.0" + +broccoli-concat@^2.0.4, broccoli-concat@^2.2.0: + version "2.3.8" + resolved "https://registry.yarnpkg.com/broccoli-concat/-/broccoli-concat-2.3.8.tgz#590cdcc021bb905b6c121d87c2d1d57df44a2a48" + dependencies: + broccoli-caching-writer "^2.3.1" + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-stew "^1.3.3" + fast-sourcemap-concat "^1.0.1" + fs-extra "^0.30.0" + lodash.merge "^4.3.0" + lodash.omit "^4.1.0" + lodash.uniq "^4.2.0" + +broccoli-config-loader@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/broccoli-config-loader/-/broccoli-config-loader-1.0.0.tgz#c3cf5ecfaffc04338c6f1d5d38dc36baeaa131ba" + dependencies: + broccoli-caching-writer "^2.0.4" + +broccoli-config-replace@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/broccoli-config-replace/-/broccoli-config-replace-1.1.2.tgz#6ea879d92a5bad634d11329b51fc5f4aafda9c00" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^1.2.0" + debug "^2.2.0" + fs-extra "^0.24.0" + +broccoli-file-creator@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/broccoli-file-creator/-/broccoli-file-creator-1.1.1.tgz#1b35b67d215abdfadd8d49eeb69493c39e6c3450" + dependencies: + broccoli-kitchen-sink-helpers "~0.2.0" + broccoli-plugin "^1.1.0" + broccoli-writer "~0.1.1" + mkdirp "^0.5.1" + rsvp "~3.0.6" + symlink-or-copy "^1.0.1" + +broccoli-filter@^1.2.2, broccoli-filter@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/broccoli-filter/-/broccoli-filter-1.2.4.tgz#409afb94b9a3a6da9fac8134e91e205f40cc7330" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^1.0.0" + copy-dereference "^1.0.0" + debug "^2.2.0" + mkdirp "^0.5.1" + promise-map-series "^0.2.1" + rsvp "^3.0.18" + symlink-or-copy "^1.0.1" + walk-sync "^0.3.1" + +broccoli-funnel-reducer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/broccoli-funnel-reducer/-/broccoli-funnel-reducer-1.0.0.tgz#11365b2a785aec9b17972a36df87eef24c5cc0ea" + +broccoli-funnel@^1.0.0, broccoli-funnel@^1.0.1, broccoli-funnel@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-1.1.0.tgz#dfb91a37c902456456de4a40a1881948d65b27d9" + dependencies: + array-equal "^1.0.0" + blank-object "^1.0.1" + broccoli-plugin "^1.3.0" + debug "^2.2.0" + exists-sync "0.0.4" + fast-ordered-set "^1.0.0" + fs-tree-diff "^0.5.3" + heimdalljs "^0.2.0" + minimatch "^3.0.0" + mkdirp "^0.5.0" + path-posix "^1.0.0" + rimraf "^2.4.3" + symlink-or-copy "^1.0.0" + walk-sync "^0.3.1" + +broccoli-jshint@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/broccoli-jshint/-/broccoli-jshint-1.2.0.tgz#8cd565d11a04bfd32cb8f85a0f7ede1e5be7a6a2" + dependencies: + broccoli-persistent-filter "^1.2.0" + chalk "~0.4.0" + findup-sync "^0.3.0" + jshint "^2.7.0" + json-stable-stringify "^1.0.0" + mkdirp "~0.4.0" + +broccoli-kitchen-sink-helpers@^0.2.5, broccoli-kitchen-sink-helpers@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/broccoli-kitchen-sink-helpers/-/broccoli-kitchen-sink-helpers-0.2.9.tgz#a5e0986ed8d76fb5984b68c3f0450d3a96e36ecc" + dependencies: + glob "^5.0.10" + mkdirp "^0.5.1" + +broccoli-kitchen-sink-helpers@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/broccoli-kitchen-sink-helpers/-/broccoli-kitchen-sink-helpers-0.3.1.tgz#77c7c18194b9664163ec4fcee2793444926e0c06" + dependencies: + glob "^5.0.10" + mkdirp "^0.5.1" + +broccoli-lint-eslint@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/broccoli-lint-eslint/-/broccoli-lint-eslint-3.1.0.tgz#0b2f6ff20db081f3cc673050a754899b37afe5ab" + dependencies: + broccoli-persistent-filter "^1.2.0" + escape-string-regexp "^1.0.5" + eslint "^3.0.0" + json-stable-stringify "^1.0.1" + md5-hex "^1.2.1" + +broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-1.2.1.tgz#16a7494ed56dbe61611f6c2d4817cfbaad2a3055" + dependencies: + broccoli-plugin "^1.3.0" + can-symlink "^1.0.0" + fast-ordered-set "^1.0.2" + fs-tree-diff "^0.5.4" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + rimraf "^2.4.3" + symlink-or-copy "^1.0.0" + +broccoli-persistent-filter@^1.0.1, broccoli-persistent-filter@^1.0.3, broccoli-persistent-filter@^1.1.6, broccoli-persistent-filter@^1.2.0: + version "1.2.11" + resolved "https://registry.yarnpkg.com/broccoli-persistent-filter/-/broccoli-persistent-filter-1.2.11.tgz#95cc6b0b0eb0dcce5f8e6ae18f6a3cc45a06bf40" + dependencies: + async-disk-cache "^1.0.0" + blank-object "^1.0.1" + broccoli-plugin "^1.0.0" + fs-tree-diff "^0.5.2" + hash-for-dep "^1.0.2" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + md5-hex "^1.0.2" + mkdirp "^0.5.1" + promise-map-series "^0.2.1" + rsvp "^3.0.18" + symlink-or-copy "^1.0.1" + walk-sync "^0.3.1" + +broccoli-plugin@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.1.0.tgz#73e2cfa05f8ea1e3fc1420c40c3d9e7dc724bf02" + dependencies: + promise-map-series "^0.2.1" + quick-temp "^0.1.3" + rimraf "^2.3.4" + symlink-or-copy "^1.0.1" + +broccoli-plugin@^1.0.0, broccoli-plugin@^1.1.0, broccoli-plugin@^1.2.0, broccoli-plugin@^1.2.1, broccoli-plugin@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.3.0.tgz#bee704a8e42da08cb58e513aaa436efb7f0ef1ee" + dependencies: + promise-map-series "^0.2.1" + quick-temp "^0.1.3" + rimraf "^2.3.4" + symlink-or-copy "^1.1.8" + +broccoli-sane-watcher@^1.1.1: + version "1.1.5" + resolved "https://registry.yarnpkg.com/broccoli-sane-watcher/-/broccoli-sane-watcher-1.1.5.tgz#f2b0af9cf0afb74c7a49cd88eb11c6869ee8c0c0" + dependencies: + broccoli-slow-trees "^1.1.0" + debug "^2.1.0" + rsvp "^3.0.18" + sane "^1.1.1" + +broccoli-sass-source-maps@^1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/broccoli-sass-source-maps/-/broccoli-sass-source-maps-1.8.1.tgz#115e32be25dc5f1686af1c8d1fa4c4c62749f0b6" + dependencies: + broccoli-caching-writer "^3.0.3" + include-path-searcher "^0.1.0" + mkdirp "^0.3.5" + node-sass "^3.8.0" + object-assign "^2.0.0" + rsvp "^3.0.6" + +broccoli-slow-trees@^1.0.0, broccoli-slow-trees@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-slow-trees/-/broccoli-slow-trees-1.1.0.tgz#426c5724e008107e4573f73e8a9ca702916b78f7" + +broccoli-source@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-source/-/broccoli-source-1.1.0.tgz#54f0e82c8b73f46580cbbc4f578f0b32fca8f809" + +broccoli-sri-hash@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/broccoli-sri-hash/-/broccoli-sri-hash-2.1.2.tgz#bc69905ed7a381ad325cc0d02ded071328ebf3f3" + dependencies: + broccoli-caching-writer "^2.2.0" + mkdirp "^0.5.1" + rsvp "^3.1.0" + sri-toolbox "^0.2.0" + symlink-or-copy "^1.0.1" + +broccoli-stew@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/broccoli-stew/-/broccoli-stew-1.4.0.tgz#1bdb0a1804d62a419d190abc26acb3c91878154d" + dependencies: + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^1.0.0" + broccoli-persistent-filter "^1.1.6" + chalk "^1.1.3" + debug "^2.1.0" + ensure-posix-path "^1.0.1" + fs-extra "^0.30.0" + minimatch "^3.0.2" + resolve "^1.1.6" + rsvp "^3.0.16" + walk-sync "^0.3.0" + +broccoli-uglify-sourcemap@^1.0.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/broccoli-uglify-sourcemap/-/broccoli-uglify-sourcemap-1.4.2.tgz#1e280afbdfaa700b2f42155f6c4a036c37e61ca7" + dependencies: + broccoli-plugin "^1.2.1" + debug "^2.2.0" + lodash.merge "^4.5.1" + matcher-collection "^1.0.0" + mkdirp "^0.5.0" + source-map-url "^0.3.0" + symlink-or-copy "^1.0.1" + uglify-js "^2.6.0" + walk-sync "^0.1.3" + +broccoli-unwatched-tree@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/broccoli-unwatched-tree/-/broccoli-unwatched-tree-0.1.1.tgz#4312fde04bdafe67a05a967d72cc50b184a9f514" + +broccoli-viz@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/broccoli-viz/-/broccoli-viz-2.0.1.tgz#3f3ed2fb83e368aa5306fae460801dea552e40db" + +broccoli-writer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/broccoli-writer/-/broccoli-writer-0.1.1.tgz#d4d71aa8f2afbc67a3866b91a2da79084b96ab2d" + dependencies: + quick-temp "^0.1.0" + rsvp "^3.0.6" + +bser@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169" + dependencies: + node-int64 "^0.4.0" + +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtins@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-0.0.7.tgz#355219cd6cf18dbe7c01cc7fd2dce765cfdc549a" + +bytes@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588" + +bytes@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2, camelcase@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +can-symlink@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/can-symlink/-/can-symlink-1.0.0.tgz#97b607d8a84bb6c6e228b902d864ecb594b9d219" + dependencies: + tmp "0.0.28" + +cardinal@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-0.5.0.tgz#00d5f661dbd4aabfdf7d41ce48a5a59bca35a291" + dependencies: + ansicolors "~0.2.1" + redeyed "~0.5.0" + +cardinal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" + dependencies: + ansicolors "~0.2.1" + redeyed "~1.0.0" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" + dependencies: + ansi-styles "^1.1.0" + escape-string-regexp "^1.0.0" + has-ansi "^0.1.0" + strip-ansi "^0.3.0" + supports-color "^0.2.0" + +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" + dependencies: + ansi-styles "~1.0.0" + has-color "~0.1.0" + strip-ansi "~0.1.0" + +char-spinner@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081" + +charm@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + dependencies: + inherits "^2.0.1" + +chmodr@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chmodr/-/chmodr-1.0.2.tgz#04662b932d0f02ec66deaa2b0ea42811968e3eb9" + +chownr@^1.0.1, chownr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +circular-json@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + +clean-base-url@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clean-base-url/-/clean-base-url-1.0.0.tgz#c901cf0a20b972435b0eccd52d056824a4351b7b" + +clean-css-promise@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/clean-css-promise/-/clean-css-promise-0.1.1.tgz#43f3d2c8dfcb2bf071481252cd9b76433c08eecb" + dependencies: + array-to-error "^1.0.0" + clean-css "^3.4.5" + pinkie-promise "^2.0.0" + +clean-css@^3.4.5: + version "3.4.21" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.21.tgz#2101d5dbd19d63dbc16a75ebd570e7c33948f65b" + dependencies: + commander "2.8.x" + source-map "0.4.x" + +cli-cursor@^1.0.1, cli-cursor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-spinners@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + +cli-table2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" + dependencies: + lodash "^3.10.1" + string-width "^1.0.1" + optionalDependencies: + colors "^1.1.2" + +cli-table@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" + dependencies: + colors "1.0.3" + +cli-usage@^0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.4.tgz#7c01e0dc706c234b39c933838c8e20b2175776e2" + dependencies: + marked "^0.3.6" + marked-terminal "^1.6.2" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + +cli@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14" + dependencies: + exit "0.1.2" + glob "^7.1.1" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + +clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +cmd-shim@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" + dependencies: + graceful-fs "^4.1.2" + mkdirp "~0.5.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +colors@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +colors@~0.6.0-1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" + +columnify@~1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" + dependencies: + strip-ansi "^3.0.0" + wcwidth "^1.0.0" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@2.8.x: + version "2.8.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commander@^2.5.0, commander@^2.6.0, commander@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commander@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" + +commoner@~0.10.3: + version "0.10.8" + resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" + dependencies: + commander "^2.5.0" + detective "^4.3.1" + glob "^5.0.15" + graceful-fs "^4.1.2" + iconv-lite "^0.4.5" + mkdirp "^0.5.0" + private "^0.1.6" + q "^1.1.2" + recast "^0.11.17" + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + +component-emitter@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3" + +component-emitter@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.0.tgz#ccd113a86388d06482d03de3fc7df98526ba8efe" + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + +compressible@~2.0.8: + version "2.0.9" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.9.tgz#6daab4e2b599c2770dd9e21e7a891b1c5a755425" + dependencies: + mime-db ">= 1.24.0 < 2" + +compression@^1.4.4: + version "1.6.2" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3" + dependencies: + accepts "~1.3.3" + bytes "2.3.0" + compressible "~2.0.8" + debug "~2.2.0" + on-headers "~1.0.1" + vary "~1.1.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6, concat-stream@^1.4.7: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + +config-chain@~1.1.10: + version "1.1.11" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1" + dependencies: + dot-prop "^3.0.0" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + object-assign "^4.0.1" + os-tmpdir "^1.0.0" + osenv "^0.1.0" + uuid "^2.0.1" + write-file-atomic "^1.1.2" + xdg-basedir "^2.0.0" + +connect@^3.3.3: + version "3.5.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.5.0.tgz#b357525a0b4c1f50599cd983e1d9efeea9677198" + dependencies: + debug "~2.2.0" + finalhandler "0.5.0" + parseurl "~1.3.1" + utils-merge "1.0.0" + +console-browserify@1.1.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +consolidate@^0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" + dependencies: + bluebird "^3.1.1" + +content-disposition@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" + +content-type@~1.0.1, content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +convert-source-map@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +copy-dereference@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/copy-dereference/-/copy-dereference-1.0.0.tgz#6b131865420fd81b413ba994b44d3655311152b6" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +core-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/core-object/-/core-object-1.1.0.tgz#86d63918733cf9da1a5aae729e62c0a88e66ad0a" + +core-object@^2.0.2: + version "2.0.6" + resolved "https://registry.yarnpkg.com/core-object/-/core-object-2.0.6.tgz#60134b9c40ff69b27bc15e82db945e4df782961b" + dependencies: + chalk "^1.1.3" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +d@^0.1.1, d@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" + dependencies: + es5-ext "~0.10.2" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" + +debug@2.2.0, debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" + dependencies: + ms "0.7.2" + +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + dependencies: + clone "^1.0.2" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +defs@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/defs/-/defs-1.1.1.tgz#b22609f2c7a11ba7a3db116805c139b1caffa9d2" + dependencies: + alter "~0.2.0" + ast-traverse "~0.1.1" + breakable "~1.0.0" + esprima-fb "~15001.1001.0-dev-harmony-fb" + simple-fmt "~0.1.0" + simple-is "~0.2.0" + stringmap "~0.2.2" + stringset "~0.2.1" + tryor "~0.1.2" + yargs "~3.27.0" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-indent@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-3.0.1.tgz#9dc5e5ddbceef8325764b9451b02bc6d54084f75" + dependencies: + get-stdin "^4.0.1" + minimist "^1.1.0" + repeating "^1.1.0" + +detective@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/detective/-/detective-4.3.2.tgz#77697e2e7947ac3fe7c8e26a6d6f115235afa91c" + dependencies: + acorn "^3.1.0" + defined "^1.0.0" + +dezalgo@^1.0.0, dezalgo@^1.0.1, dezalgo@^1.0.2, dezalgo@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + dependencies: + asap "^2.0.0" + wrappy "1" + +did_it_work@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/did_it_work/-/did_it_work-0.0.6.tgz#5180cb9e16ebf9a8753a0cc6b4af9ccdff71ec05" + +diff@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +dom-serializer@0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domelementtype@1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" + dependencies: + domelementtype "1" + +domutils@1.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" + dependencies: + is-obj "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +editions@^1.1.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.3.tgz#0907101bdda20fac3cbe334c27cbd0688dc99a5b" + +editor@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +ember-bootstrap@~0.11.2: + version "0.11.3" + resolved "https://registry.yarnpkg.com/ember-bootstrap/-/ember-bootstrap-0.11.3.tgz#810d9e7202cfb5439d731360589a62144087e96c" + dependencies: + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^1.1.1" + ember-cli-babel "^5.1.6" + ember-cli-htmlbars "^1.1.0" + ember-runtime-enumerable-includes-polyfill "^1.0.1" + ember-wormhole "^0.4.1" + +ember-buffered-proxy@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/ember-buffered-proxy/-/ember-buffered-proxy-0.6.0.tgz#4575bf8a16e4ac28711e3f577789844f65a14409" + dependencies: + ember-cli-babel "^5.1.6" + +ember-busy-blocker@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ember-busy-blocker/-/ember-busy-blocker-0.1.0.tgz#b6ffbe55440fcb26504ae516f409ee9179692dc5" + dependencies: + ember-cli-babel "^5.1.5" + ember-cli-htmlbars "^1.0.1" + +ember-cli-babel@5.1.10, ember-cli-babel@^5.1.10, ember-cli-babel@^5.1.3, ember-cli-babel@^5.1.5, ember-cli-babel@^5.1.6, ember-cli-babel@^5.1.7, ember-cli-babel@^5.1.8: + version "5.1.10" + resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-5.1.10.tgz#d403f178aab602e1337c403c5a58c0200a8969aa" + dependencies: + broccoli-babel-transpiler "^5.6.0" + broccoli-funnel "^1.0.0" + clone "^1.0.2" + ember-cli-version-checker "^1.0.2" + resolve "^1.1.2" + +ember-cli-broccoli@0.16.10: + version "0.16.10" + resolved "https://registry.yarnpkg.com/ember-cli-broccoli/-/ember-cli-broccoli-0.16.10.tgz#e1f6fb204fc04bba836edaf3fdd07f4bec868d1b" + dependencies: + broccoli-kitchen-sink-helpers "^0.2.5" + broccoli-slow-trees "^1.0.0" + commander "^2.5.0" + connect "^3.3.3" + copy-dereference "^1.0.0" + findup-sync "^0.2.1" + handlebars "^4.0.4" + mime "^1.2.11" + promise-map-series "^0.2.1" + quick-temp "^0.1.2" + rimraf "^2.2.8" + rsvp "^3.0.17" + +ember-cli-dependency-checker@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ember-cli-dependency-checker/-/ember-cli-dependency-checker-1.3.0.tgz#f0e8cb7f0f43c1e560494eaa9372804e7a088a2a" + dependencies: + chalk "^0.5.1" + is-git-url "0.2.0" + semver "^4.1.0" + +ember-cli-eslint@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-eslint/-/ember-cli-eslint-3.0.0.tgz#83eb12b01e1bd41ce7d35fbd6dd18136faa1398b" + dependencies: + broccoli-lint-eslint "^3.1.0" + ember-cli-babel "^5.1.6" + js-string-escape "^1.0.0" + rsvp "^3.2.1" + walk-sync "^0.3.0" + +ember-cli-format-number@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-format-number/-/ember-cli-format-number-2.0.0.tgz#db744b0578484529b68c0068bbd057e637007ec0" + dependencies: + ember-cli-babel "^5.1.5" + ember-cli-node-assets "^0.1.4" + numeral "^1.5.3" + +ember-cli-get-component-path-option@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-get-component-path-option/-/ember-cli-get-component-path-option-1.0.0.tgz#0d7b595559e2f9050abed804f1d8eff1b08bc771" + +ember-cli-get-dependency-depth@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-get-dependency-depth/-/ember-cli-get-dependency-depth-1.0.0.tgz#e0afecf82a2d52f00f28ab468295281aec368d11" + +ember-cli-htmlbars-inline-precompile@^0.3.1: + version "0.3.6" + resolved "https://registry.yarnpkg.com/ember-cli-htmlbars-inline-precompile/-/ember-cli-htmlbars-inline-precompile-0.3.6.tgz#4095fe423f93102724c0725e4dd1a31f25e24de5" + dependencies: + babel-plugin-htmlbars-inline-precompile "^0.1.0" + ember-cli-babel "^5.1.3" + ember-cli-htmlbars "^1.0.0" + hash-for-dep "^1.0.2" + +ember-cli-htmlbars@^1.0.0, ember-cli-htmlbars@^1.0.1, ember-cli-htmlbars@^1.0.10, ember-cli-htmlbars@^1.0.3, ember-cli-htmlbars@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-1.1.1.tgz#8776cf59796dac8f32e8625fc6d1ea45ffa55de1" + dependencies: + broccoli-persistent-filter "^1.0.3" + ember-cli-version-checker "^1.0.2" + hash-for-dep "^1.0.2" + json-stable-stringify "^1.0.0" + strip-bom "^2.0.0" + +ember-cli-is-package-missing@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-is-package-missing/-/ember-cli-is-package-missing-1.0.0.tgz#6e6184cafb92635dd93ca6c946b104292d4e3390" + +ember-cli-legacy-blueprints@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/ember-cli-legacy-blueprints/-/ember-cli-legacy-blueprints-0.1.3.tgz#caf477a775229a0cd6a28b659304943db1367770" + dependencies: + chalk "^1.1.1" + ember-cli-get-component-path-option "^1.0.0" + ember-cli-get-dependency-depth "^1.0.0" + ember-cli-is-package-missing "^1.0.0" + ember-cli-lodash-subset "^1.0.7" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-path-utils "^1.0.0" + ember-cli-string-utils "^1.0.0" + ember-cli-test-info "^1.0.0" + ember-cli-valid-component-name "^1.0.0" + ember-cli-version-checker "^1.1.7" + ember-router-generator "^1.0.0" + exists-sync "0.0.3" + fs-extra "^0.24.0" + inflection "^1.7.1" + rsvp "^3.0.17" + silent-error "^1.0.0" + +ember-cli-lodash-subset@^1.0.1, ember-cli-lodash-subset@^1.0.7: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ember-cli-lodash-subset/-/ember-cli-lodash-subset-1.0.11.tgz#0149eef9c0c3505ba44ed202f8d034ddbbfb2826" + +ember-cli-node-assets@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ember-cli-node-assets/-/ember-cli-node-assets-0.1.6.tgz#6488a2949048c801ad6d9e33753c7bce32fc1146" + dependencies: + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^1.1.1" + broccoli-unwatched-tree "^0.1.1" + debug "^2.2.0" + lodash "^4.5.1" + resolve "^1.1.7" + +ember-cli-normalize-entity-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-normalize-entity-name/-/ember-cli-normalize-entity-name-1.0.0.tgz#0b14f7bcbc599aa117b5fddc81e4fd03c4bad5b7" + dependencies: + silent-error "^1.0.0" + +ember-cli-path-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-path-utils/-/ember-cli-path-utils-1.0.0.tgz#4e39af8b55301cddc5017739b77a804fba2071ed" + +ember-cli-preprocess-registry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-preprocess-registry/-/ember-cli-preprocess-registry-2.0.0.tgz#45c8b985eba06bb443b3abce1c3c6220fdcb8094" + dependencies: + broccoli-clean-css "^1.1.0" + broccoli-funnel "^1.0.0" + broccoli-merge-trees "^1.0.0" + debug "^2.2.0" + exists-sync "0.0.3" + lodash "^3.10.0" + process-relative-require "^1.0.0" + silent-error "^1.0.0" + +ember-cli-qunit@^1.4.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/ember-cli-qunit/-/ember-cli-qunit-1.4.2.tgz#7ca25495c70ca347106d44fc00f0d7aeca027475" + dependencies: + broccoli-babel-transpiler "^5.5.0" + broccoli-concat "^2.2.0" + broccoli-jshint "^1.0.0" + broccoli-merge-trees "^1.1.0" + ember-cli-babel "^5.1.5" + ember-cli-version-checker "^1.1.4" + ember-qunit "^0.4.18" + qunitjs "^1.20.0" + resolve "^1.1.6" + +ember-cli-sass@~5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/ember-cli-sass/-/ember-cli-sass-5.6.0.tgz#792de67544bb903eef421a3e59c484840fea5352" + dependencies: + broccoli-funnel "^1.0.0" + broccoli-merge-trees "^1.1.0" + broccoli-sass-source-maps "^1.8.0" + ember-cli-babel "5.1.10" + ember-cli-version-checker "^1.0.2" + merge "^1.2.0" + +ember-cli-sri@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ember-cli-sri/-/ember-cli-sri-2.1.1.tgz#971620934a4b9183cf7923cc03e178b83aa907fd" + dependencies: + broccoli-sri-hash "^2.1.0" + +ember-cli-string-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-string-utils/-/ember-cli-string-utils-1.0.0.tgz#d07b17d0b6223c42e09bfb835ee2b8466ec9b88e" + +ember-cli-test-info@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-test-info/-/ember-cli-test-info-1.0.0.tgz#ed4e960f249e97523cf891e4aed2072ce84577b4" + dependencies: + ember-cli-string-utils "^1.0.0" + +ember-cli-uglify@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ember-cli-uglify/-/ember-cli-uglify-1.2.0.tgz#3208c32b54bc2783056e8bb0d5cfe9bbaf17ffb2" + dependencies: + broccoli-uglify-sourcemap "^1.0.0" + +ember-cli-valid-component-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-valid-component-name/-/ember-cli-valid-component-name-1.0.0.tgz#71550ce387e0233065f30b30b1510aa2dfbe87ef" + dependencies: + silent-error "^1.0.0" + +ember-cli-version-checker@^1.0.2, ember-cli-version-checker@^1.1.3, ember-cli-version-checker@^1.1.4, ember-cli-version-checker@^1.1.6, ember-cli-version-checker@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-1.1.7.tgz#cbabacb5eef2635048d5208fab05b032e5313d1a" + dependencies: + semver "^4.2.2" + +ember-cli@~2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/ember-cli/-/ember-cli-2.8.0.tgz#7c503a49cbd8423ba58403e222056bdfb79a985a" + dependencies: + amd-name-resolver "0.0.5" + bower "^1.3.12" + bower-config "^1.3.0" + bower-endpoint-parser "0.2.2" + broccoli-babel-transpiler "^5.4.5" + broccoli-concat "^2.0.4" + broccoli-config-loader "^1.0.0" + broccoli-config-replace "^1.1.2" + broccoli-funnel "^1.0.3" + broccoli-funnel-reducer "^1.0.0" + broccoli-merge-trees "^1.1.2" + broccoli-sane-watcher "^1.1.1" + broccoli-source "^1.1.0" + broccoli-viz "^2.0.1" + chalk "^1.1.3" + clean-base-url "^1.0.0" + compression "^1.4.4" + configstore "^2.0.0" + core-object "^2.0.2" + debug "^2.1.3" + diff "^1.3.1" + ember-cli-broccoli "0.16.10" + ember-cli-get-component-path-option "^1.0.0" + ember-cli-is-package-missing "^1.0.0" + ember-cli-legacy-blueprints "^0.1.1" + ember-cli-lodash-subset "^1.0.1" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-preprocess-registry "^2.0.0" + ember-cli-string-utils "^1.0.0" + ember-try "^0.2.2" + escape-string-regexp "^1.0.3" + exists-sync "0.0.3" + exit "^0.1.2" + express "^4.12.3" + filesize "^3.1.3" + find-up "^1.1.2" + fs-extra "0.30.0" + fs-monitor-stack "^1.0.2" + fs-tree-diff "^0.5.0" + get-caller-file "^1.0.0" + git-repo-info "^1.0.4" + glob "7.0.5" + http-proxy "^1.9.0" + inflection "^1.7.0" + inquirer "^1.0.2" + is-git-url "^0.2.0" + isbinaryfile "^3.0.0" + leek "0.0.22" + lodash "^4.12.0" + lodash.template "^4.2.5" + markdown-it "7.0.0" + markdown-it-terminal "0.0.3" + minimatch "^3.0.0" + morgan "^1.5.2" + node-modules-path "^1.0.0" + node-uuid "^1.4.3" + nopt "^3.0.1" + npm "2.15.5" + npm-package-arg "^4.1.1" + ora "^0.2.0" + portfinder "^1.0.4" + promise-map-series "^0.2.1" + quick-temp "0.1.5" + resolve "^1.1.6" + rsvp "^3.0.17" + sane "^1.1.1" + semver "^5.1.1" + silent-error "^1.0.0" + symlink-or-copy "^1.0.1" + temp "0.8.3" + testem "^1.8.1" + through "^2.3.6" + tiny-lr "0.2.1" + tree-sync "^1.1.0" + walk-sync "^0.2.6" + yam "0.0.21" + +ember-cp-validations@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/ember-cp-validations/-/ember-cp-validations-3.1.2.tgz#53642655b371fc9f8cff72f05304fe043415094a" + dependencies: + ember-cli-babel "^5.1.8" + ember-cli-version-checker "^1.1.4" + ember-getowner-polyfill "^1.0.1" + ember-require-module "^0.1.1" + ember-string-ishtmlsafe-polyfill "1.0.1" + ember-validators "^0.1.2" + exists-sync "0.0.3" + walk-sync "^0.3.1" + +ember-data@~2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/ember-data/-/ember-data-2.8.1.tgz#ec09afaeab76df351dc561ce2ee1eb92f5bca8ff" + dependencies: + amd-name-resolver "0.0.5" + babel-plugin-feature-flags "^0.2.1" + babel-plugin-filter-imports "^0.2.0" + broccoli-babel-transpiler "^5.5.0" + broccoli-file-creator "^1.0.0" + broccoli-merge-trees "^1.0.0" + chalk "^1.1.1" + ember-cli-babel "^5.1.6" + ember-cli-path-utils "^1.0.0" + ember-cli-string-utils "^1.0.0" + ember-cli-test-info "^1.0.0" + ember-cli-version-checker "^1.1.4" + ember-inflector "^1.9.4" + ember-runtime-enumerable-includes-polyfill "^1.0.0" + exists-sync "0.0.3" + git-repo-info "^1.1.2" + inflection "^1.8.0" + npm-git-info "^1.0.0" + semver "^5.1.0" + silent-error "^1.0.0" + +ember-export-application-global@^1.0.5: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ember-export-application-global/-/ember-export-application-global-1.1.1.tgz#f257d5271268932a89d7392679ce4db89d7154af" + dependencies: + ember-cli-babel "^5.1.10" + +ember-get-helper@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ember-get-helper/-/ember-get-helper-1.1.0.tgz#ef970b199bc1b6a52e986cb744ddf05eb3793051" + dependencies: + ember-cli-babel "^5.1.6" + ember-cli-version-checker "^1.1.3" + +ember-getowner-polyfill@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-getowner-polyfill/-/ember-getowner-polyfill-1.0.0.tgz#f847ceabd97ab97e9e9d279c382400a4d407a9d6" + dependencies: + ember-cli-babel "^5.1.5" + +ember-getowner-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ember-getowner-polyfill/-/ember-getowner-polyfill-1.0.1.tgz#f60a31d25d642461dac4b4746184afaf7f5084ae" + dependencies: + ember-cli-babel "^5.1.5" + +ember-inflector@^1.9.4: + version "1.9.6" + resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-1.9.6.tgz#574b093664ade6c66e84185d788ec99ed29741af" + dependencies: + ember-cli-babel "^5.1.6" + +ember-invoke-action@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ember-invoke-action/-/ember-invoke-action-1.4.0.tgz#2899854bd755f9331ca86c902bf6d4dbf8bdfcb3" + dependencies: + ember-cli-babel "^5.1.6" + +ember-load-initializers@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-0.5.1.tgz#76e3db23c111dbdcd3ae6f687036bf0b56be0cbe" + +ember-load@~0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/ember-load/-/ember-load-0.0.11.tgz#8f8a243bb513a1ac765270ba98759e92355fb873" + dependencies: + ember-cli-babel "^5.1.7" + ember-cli-htmlbars "^1.0.10" + ember-cli-version-checker "^1.1.4" + +ember-multiselect-checkboxes@~0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/ember-multiselect-checkboxes/-/ember-multiselect-checkboxes-0.10.1.tgz#524a340624ca8d141c9b2b7823410aa26d53dc10" + dependencies: + ember-cli-babel "^5.1.6" + ember-runtime-enumerable-includes-polyfill "^1.0.1" + +ember-onbeforeunload@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ember-onbeforeunload/-/ember-onbeforeunload-1.1.0.tgz#7e93fe697add0c1f3099758e9b95b5ebd91400ff" + dependencies: + ember-cli-babel "^5.1.6" + +ember-one-way-controls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ember-one-way-controls/-/ember-one-way-controls-1.1.2.tgz#9ebaba62bb43bf3d5e680f0d13b996353e47ce6d" + dependencies: + ember-cli-babel "^5.1.6" + ember-cli-htmlbars "^1.0.10" + ember-get-helper "~1.1.0" + ember-invoke-action "1.4.0" + ember-runtime-enumerable-includes-polyfill "^1.0.1" + +ember-qunit@^0.4.18: + version "0.4.22" + resolved "https://registry.yarnpkg.com/ember-qunit/-/ember-qunit-0.4.22.tgz#bc389798e1a4a933f542863025e2fb91d856da49" + dependencies: + ember-test-helpers "^0.5.32" + +ember-require-module@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ember-require-module/-/ember-require-module-0.1.0.tgz#c1791479e3a36d0c4db9294d7039d23188cb12a9" + dependencies: + ember-cli-babel "^5.1.7" + +ember-require-module@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ember-require-module/-/ember-require-module-0.1.1.tgz#5a2130f691ad1578b1d236dd11196721fd5526b2" + dependencies: + ember-cli-babel "^5.1.7" + +ember-resolver@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ember-resolver/-/ember-resolver-2.1.0.tgz#e6d249d3b5806468c0ba2c67ccecf7aa24b754bf" + dependencies: + ember-cli-babel "^5.1.3" + ember-cli-version-checker "^1.1.4" + +ember-router-generator@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-1.2.2.tgz#62dac1f63e873553e6d4c7e32da6589e577bcf63" + dependencies: + recast "^0.11.3" + +ember-runtime-enumerable-includes-polyfill@^1.0.0, ember-runtime-enumerable-includes-polyfill@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ember-runtime-enumerable-includes-polyfill/-/ember-runtime-enumerable-includes-polyfill-1.0.4.tgz#16a7612e347a2edf07da8b2f2f09dbfee70deba0" + dependencies: + ember-cli-babel "^5.1.6" + ember-cli-version-checker "^1.1.6" + +ember-simple-auth@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ember-simple-auth/-/ember-simple-auth-1.1.0.tgz#7a6162e1b58b88c4b535e4476be6033be233b2b6" + dependencies: + ember-cli-babel "^5.1.5" + ember-cli-is-package-missing "^1.0.0" + ember-getowner-polyfill "1.0.0" + silent-error "^1.0.0" + +ember-string-ishtmlsafe-polyfill@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ember-string-ishtmlsafe-polyfill/-/ember-string-ishtmlsafe-polyfill-1.0.1.tgz#b40a8c98663be5c16d16f31dc686102bb655abbd" + dependencies: + ember-cli-babel "^5.1.6" + +ember-test-helpers@^0.5.32: + version "0.5.34" + resolved "https://registry.yarnpkg.com/ember-test-helpers/-/ember-test-helpers-0.5.34.tgz#c8439108d1cba1d7d838c212208a5c4061471b83" + dependencies: + klassy "^0.1.3" + +ember-truth-helpers@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-1.2.0.tgz#e63cffeaa8211882ae61a958816fded3790d065b" + dependencies: + ember-cli-babel "^5.1.5" + +ember-try-config@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ember-try-config/-/ember-try-config-2.1.0.tgz#e0e156229a542346a58ee6f6ad605104c98edfe0" + dependencies: + lodash "^4.6.1" + node-fetch "^1.3.3" + rsvp "^3.2.1" + semver "^5.1.0" + +ember-try@^0.2.2: + version "0.2.8" + resolved "https://registry.yarnpkg.com/ember-try/-/ember-try-0.2.8.tgz#5f135d23d83561dc8dfb4a4d998420b69b740acd" + dependencies: + chalk "^1.0.0" + cli-table2 "^0.2.0" + core-object "^1.1.0" + debug "^2.2.0" + ember-cli-babel "^5.1.3" + ember-cli-version-checker "^1.1.6" + ember-try-config "^2.0.1" + extend "^3.0.0" + fs-extra "^0.26.0" + promise-map-series "^0.2.1" + resolve "^1.1.6" + rimraf "^2.3.2" + rsvp "^3.0.17" + semver "^5.1.0" + sync-exec "^0.6.2" + +ember-validators@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ember-validators/-/ember-validators-0.1.2.tgz#b26cc93f3d6f3ec7042bc5f069c9794a0ddd2357" + dependencies: + ember-cli-babel "^5.1.6" + ember-require-module "0.1.0" + +ember-wormhole@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/ember-wormhole/-/ember-wormhole-0.4.1.tgz#55fafaad20a650d21f6583a0e59c060a65338111" + dependencies: + ember-cli-babel "^5.1.6" + ember-cli-htmlbars "^1.0.3" + +emberx-select@~2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/emberx-select/-/emberx-select-2.2.2.tgz#43837c467b64e52235142c53e5112dbd57082bed" + dependencies: + ember-cli-babel "^5.1.6" + ember-cli-htmlbars "^1.0.3" + ember-cli-version-checker "^1.1.6" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +engine.io-client@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.7.0.tgz#0bb81d3563ab7afb668f1e1b400c9403b03006ee" + dependencies: + component-emitter "1.1.2" + component-inherit "0.0.3" + debug "2.2.0" + engine.io-parser "1.3.0" + has-cors "1.1.0" + indexof "0.0.1" + parsejson "0.0.1" + parseqs "0.0.2" + parseuri "0.0.4" + ws "1.1.1" + xmlhttprequest-ssl "1.5.1" + yeast "0.1.2" + +engine.io-parser@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.0.tgz#61a35c7f3a3ccd1b179e4f52257a7a8cfacaeb21" + dependencies: + after "0.8.1" + arraybuffer.slice "0.0.6" + base64-arraybuffer "0.1.5" + blob "0.0.4" + has-binary "0.1.6" + wtf-8 "1.0.0" + +engine.io@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.7.0.tgz#a417857af4995d9bbdf8a0e03a87e473ebe64fbe" + dependencies: + accepts "1.3.3" + base64id "0.1.0" + debug "2.2.0" + engine.io-parser "1.3.0" + ws "1.1.1" + +ensure-posix-path@^1.0.0, ensure-posix-path@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.0.2.tgz#a65b3e42d0b71cfc585eb774f9943c8d9b91b0c2" + +entities@1.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" + +entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + +error-ex@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9" + dependencies: + is-arrayish "^0.2.1" + +es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7: + version "0.10.12" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" + dependencies: + d "^0.1.1" + es5-ext "^0.10.7" + es6-symbol "3" + +es6-map@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-set "~0.1.3" + es6-symbol "~3.1.0" + event-emitter "~0.3.4" + +es6-set@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-symbol "3" + event-emitter "~0.3.4" + +es6-symbol@3, es6-symbol@^3.0.2, es6-symbol@~3.1, es6-symbol@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + +es6-weak-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" + dependencies: + d "^0.1.1" + es5-ext "^0.10.8" + es6-iterator "2" + es6-symbol "3" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint@^3.0.0: + version "3.11.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.11.1.tgz#408be581041385cba947cd8d1cd2227782b55dbf" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + escope "^3.6.0" + espree "^3.3.1" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.2.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~1.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.3.2.tgz#dbf3fadeb4ecb4d4778303e50103b3d36c88b89c" + dependencies: + acorn "^4.0.1" + acorn-jsx "^3.0.0" + +esprima-fb@~12001.1.0-dev-harmony-fb: + version "12001.1.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-12001.1.0-dev-harmony-fb.tgz#d84400384ba95ce2678c617ad24a7f40808da915" + +esprima-fb@~15001.1001.0-dev-harmony-fb: + version "15001.1001.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659" + +esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" + +esprima@~3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.2.tgz#954b5d19321ca436092fa90f06d6798531fe8184" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + +esutils@^2.0.0, esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" + +event-emitter@~0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" + dependencies: + d "~0.1.1" + es5-ext "~0.10.7" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events-to-array@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.0.2.tgz#b3484465534fe4ff66fbdd1a83b777713ba404aa" + +exec-sh@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10" + dependencies: + merge "^1.1.3" + +exists-sync@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.0.3.tgz#b910000bedbb113b378b82f5f5a7638107622dcf" + +exists-sync@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.0.4.tgz#9744c2c428cc03b01060db454d4b12f0ef3c8879" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +exit@0.1.2, exit@0.1.x, exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + +express@^4.10.7, express@^4.12.3: + version "4.14.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.14.0.tgz#c1ee3f42cdc891fb3dc650a8922d51ec847d0d66" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.1" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "~2.2.0" + depd "~1.1.0" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + finalhandler "0.5.0" + fresh "0.3.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.2" + qs "6.2.0" + range-parser "~1.2.0" + send "0.14.1" + serve-static "~1.11.1" + type-is "~1.6.13" + utils-merge "1.0.0" + vary "~1.1.0" + +extend@^3.0.0, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +external-editor@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b" + dependencies: + extend "^3.0.0" + spawn-sync "^1.0.15" + tmp "^0.0.29" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +fast-levenshtein@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz#bd33145744519ab1c36c3ee9f31f08e9079b67f2" + +fast-ordered-set@^1.0.0, fast-ordered-set@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-ordered-set/-/fast-ordered-set-1.0.3.tgz#3fbb36634f7be79e4f7edbdb4a357dee25d184eb" + dependencies: + blank-object "^1.0.1" + +fast-sourcemap-concat@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-sourcemap-concat/-/fast-sourcemap-concat-1.1.0.tgz#a800767abed5eda02e67238ec063a709be61f9d4" + dependencies: + chalk "^0.5.1" + debug "^2.2.0" + fs-extra "^0.30.0" + memory-streams "^0.1.0" + mkdirp "^0.5.0" + rsvp "^3.0.14" + source-map "^0.4.2" + source-map-url "^0.3.0" + +faye-websocket@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^1.8.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-1.9.0.tgz#6f268f1f347a6b3c875d1e89da7e1ed79adfc0ec" + dependencies: + bser "^1.0.2" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +filesize@^3.1.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122" + +finalhandler@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" + dependencies: + debug "~2.2.0" + escape-html "~1.0.3" + on-finished "~2.3.0" + statuses "~1.3.0" + unpipe "~1.0.0" + +find-up@^1.0.0, find-up@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +findup-sync@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.2.1.tgz#e0a90a450075c49466ee513732057514b81e878c" + dependencies: + glob "~4.3.0" + +findup-sync@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" + dependencies: + glob "~5.0.0" + +findup@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/findup/-/findup-0.1.5.tgz#8ad929a3393bac627957a7e5de4623b06b0e2ceb" + dependencies: + colors "~0.6.0-1" + commander "~2.1.0" + +fireworm@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/fireworm/-/fireworm-0.7.1.tgz#ccf20f7941f108883fcddb99383dbe6e1861c758" + dependencies: + async "~0.2.9" + is-type "0.0.1" + lodash.debounce "^3.1.1" + lodash.flatten "^3.0.2" + minimatch "^3.0.2" + +flat-cache@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.1.tgz#6c837d6225a7de5659323740b36d5361f71691ff" + dependencies: + circular-json "^0.3.0" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~1.0.0-rc3: + version "1.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" + dependencies: + async "^2.0.1" + combined-stream "^1.0.5" + mime-types "^2.1.11" + +form-data@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" + +fs-extra@0.30.0, fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^0.26.0: + version "0.26.7" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-monitor-stack@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fs-monitor-stack/-/fs-monitor-stack-1.1.1.tgz#c4038d5977939b6b4e38396d7e7cd0895a7ac6b3" + +fs-readdir-recursive@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-0.1.2.tgz#315b4fb8c1ca5b8c47defef319d073dad3568059" + +fs-tree-diff@^0.5.0, fs-tree-diff@^0.5.2, fs-tree-diff@^0.5.3, fs-tree-diff@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/fs-tree-diff/-/fs-tree-diff-0.5.5.tgz#7825b4db454225dd114e7abd58e8926fe068cbff" + dependencies: + heimdalljs-logger "^0.1.7" + object-assign "^4.1.0" + path-posix "^1.0.0" + symlink-or-copy "^1.1.8" + +fs-vacuum@~1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.9.tgz#4f90193ab8ea02890995bcd4e804659a5d366b2d" + dependencies: + graceful-fs "^4.1.2" + path-is-inside "^1.0.1" + rimraf "^2.5.2" + +fs-write-stream-atomic@~1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.8.tgz#e49aaddf288f87d46ff9e882f216a13abc40778b" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fstream-ignore@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream-npm@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/fstream-npm/-/fstream-npm-1.0.7.tgz#7ed0d1ac13d7686dd9e1bf6ceb8be273bf6d2f86" + dependencies: + fstream-ignore "^1.0.0" + inherits "2" + +fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +gauge@~1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-1.2.7.tgz#e9cec5483d3d4ee0ef44b60a7d99e4935e136d93" + dependencies: + ansi "^0.3.0" + has-unicode "^2.0.0" + lodash.pad "^4.1.0" + lodash.padend "^4.1.0" + lodash.padstart "^4.1.0" + +gauge@~2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.6.0.tgz#d35301ad18e96902b4751dcbbe40f4218b942a46" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-color "^0.1.7" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gauge@~2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.1.tgz#388473894fe8be5e13ffcdb8b93e4ed0616428c7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-color "^0.1.7" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" + dependencies: + globule "^1.0.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +get-caller-file@^1.0.0, get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +getpass@^0.1.1: + version "0.1.6" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + dependencies: + assert-plus "^1.0.0" + +git-repo-info@^1.0.4, git-repo-info@^1.1.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-1.3.1.tgz#0c1a19ef1964b822a7230f087396af80481ce8ec" + +github-url-from-git@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/github-url-from-git/-/github-url-from-git-1.4.0.tgz#285e6b520819001bde128674704379e4ff03e0de" + +github-url-from-username-repo@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/github-url-from-username-repo/-/github-url-from-username-repo-1.0.2.tgz#7dd79330d2abe69c10c2cef79714c97215791dfa" + +"glob@3 || 4", glob@~4.3.0: + version "4.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.3.5.tgz#80fbb08ca540f238acce5d11d1e9bc41e75173d3" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + +glob@7.0.5, glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@~7.0.3: + version "7.0.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^5.0.10, glob@^5.0.15, glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^6.0.0: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.1, glob@~7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^6.4.0: + version "6.4.1" + resolved "https://registry.yarnpkg.com/globals/-/globals-6.4.1.tgz#8498032b3b6d1cc81eebc5f79690d8fe29fabf4f" + +globals@^9.2.0: + version "9.14.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globule@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.1.0.tgz#c49352e4dc183d85893ee825385eb994bb6df45f" + dependencies: + glob "~7.1.1" + lodash "~4.16.4" + minimatch "~3.0.2" + +graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@~4.1.4: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +growly@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + +handlebars@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +has-ansi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" + dependencies: + ansi-regex "^0.2.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-binary@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.6.tgz#25326f39cfa4f616ad8787894e3af2cfbc7b6e10" + dependencies: + isarray "0.0.1" + +has-binary@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c" + dependencies: + isarray "0.0.1" + +has-color@^0.1.7, has-color@~0.1.0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hash-for-dep@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hash-for-dep/-/hash-for-dep-1.0.3.tgz#b57f18a0ace56380951638a3b36a6b73d8619b8b" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + resolve "^1.1.6" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +heimdalljs-logger@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.7.tgz#10e340af5c22a811e34522d9b9397675ad589ca4" + dependencies: + debug "^2.2.0" + heimdalljs "^0.2.0" + +heimdalljs@^0.2.0, heimdalljs@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.3.tgz#35b82a6a4d73541fc4fb88d2fe2b23608fb4f779" + dependencies: + rsvp "~3.2.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +home-or-tmp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-1.0.0.tgz#4b9f1e40800c3e50c6c27f781676afcce71f3985" + dependencies: + os-tmpdir "^1.0.1" + user-home "^1.1.1" + +hosted-git-info@^2.1.4, hosted-git-info@^2.1.5, hosted-git-info@~2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" + +htmlparser2@3.8.x: + version "3.8.3" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" + dependencies: + domelementtype "1" + domhandler "2.3" + domutils "1.5" + entities "1.0" + readable-stream "1.1" + +http-errors@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" + dependencies: + inherits "~2.0.1" + statuses "1" + +http-errors@~1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" + dependencies: + inherits "2.0.3" + setprototypeof "1.0.2" + statuses ">= 1.3.1 < 2" + +http-proxy@^1.13.1, http-proxy@^1.9.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.0.tgz#f9b52305e9f864811835277e4a486051b5d4a523" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.13, iconv-lite@^0.4.5, iconv-lite@~0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +ignore@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435" + +imurmurhash@*, imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + +include-path-searcher@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/include-path-searcher/-/include-path-searcher-0.1.0.tgz#c0cf2ddfa164fb2eae07bc7ca43a7f191cb4d7bd" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflection@^1.7.0, inflection@^1.7.1, inflection@^1.8.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.10.0.tgz#5bffcb1197ad3e81050f8e17e21668087ee9eb2f" + +inflight@^1.0.4, inflight@~1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@^1.3.4, ini@~1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +init-package-json@~1.9.3: + version "1.9.4" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.9.4.tgz#b4053d0b40f0cf842a41966937cb3dc0f534e856" + dependencies: + glob "^6.0.0" + npm-package-arg "^4.0.0" + promzard "^0.3.0" + read "~1.0.1" + read-package-json "1 || 2" + semver "2.x || 3.x || 4 || 5" + validate-npm-package-license "^3.0.1" + validate-npm-package-name "^2.0.1" + +inline-source-map-comment@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/inline-source-map-comment/-/inline-source-map-comment-1.0.5.tgz#50a8a44c2a790dfac441b5c94eccd5462635faf6" + dependencies: + chalk "^1.0.0" + get-stdin "^4.0.1" + minimist "^1.1.1" + sum-up "^1.0.1" + xtend "^4.0.0" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^1.0.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + external-editor "^1.1.0" + figures "^1.3.5" + lodash "^4.3.0" + mute-stream "0.0.6" + pinkie-promise "^2.0.0" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ipaddr.js@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.1.1.tgz#c791d95f52b29c1247d5df80ada39b8a73647230" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-buffer@^1.0.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-git-url@0.2.0, is-git-url@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-git-url/-/is-git-url-0.2.0.tgz#b9ce0fb044821c88880213d602db03bdb255da1b" + +is-integer@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.6.tgz#5273819fada880d123e1ac00a938e7172dd8d95e" + dependencies: + is-finite "^1.0.0" + +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-type@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/is-type/-/is-type-0.0.1.tgz#f651d85c365d44955d14a51d8d7061f3f6b4779c" + dependencies: + core-util-is "~1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isbinaryfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.1.tgz#6e99573675372e841a0520c036b41513d783e79e" + +isexe@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istextorbinary@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.1.0.tgz#dbed2a6f51be2f7475b68f89465811141b758874" + dependencies: + binaryextensions "1 || 2" + editions "^1.1.1" + textextensions "1 || 2" + +jju@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +js-string-escape@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" + +js-tokens@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-1.0.1.tgz#cc435a5c8b94ad15acb7983140fc80182c89aeae" + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" + +js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.5.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +jshint@^2.7.0: + version "2.9.4" + resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.4.tgz#5e3ba97848d5290273db514aee47fe24cf592934" + dependencies: + cli "~1.0.0" + console-browserify "1.1.x" + exit "0.1.x" + htmlparser2 "3.8.x" + lodash "3.7.x" + minimatch "~3.0.2" + shelljs "0.3.x" + strip-json-comments "1.0.x" + +json-parse-helpfulerror@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz#13f14ce02eed4e981297b64eb9e3b932e2dd13dc" + dependencies: + jju "^1.1.0" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.2.6.tgz#f6efc93c06a04de9aec53053df2559bb19e2038b" + +json3@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + +json5@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.4.0.tgz#054352e4c4c80c86c0923877d449de176a732c8d" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" + +jsprim@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + dependencies: + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +kind-of@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.0.4.tgz#7b8ecf18a4e17f8269d73b501c9f232c96887a74" + dependencies: + is-buffer "^1.0.2" + +klassy@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/klassy/-/klassy-0.1.3.tgz#c31d5756d583197d75f582b6e692872be497067f" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +leek@0.0.22: + version "0.0.22" + resolved "https://registry.yarnpkg.com/leek/-/leek-0.0.22.tgz#c7b8f38963874ed2f4c0fa9b501a41fb1fb5f5af" + dependencies: + debug "^2.1.0" + lodash.assign "^3.2.0" + request "^2.27.0" + rsvp "^3.0.21" + +leven@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +linkify-it@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.2.tgz#994629a4adfa5a7d34e08c075611575ab9b6fcfc" + dependencies: + uc.micro "^1.0.1" + +linkify-it@~1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a" + dependencies: + uc.micro "^1.0.1" + +livereload-js@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +loader.js@^4.0.1: + version "4.0.11" + resolved "https://registry.yarnpkg.com/loader.js/-/loader.js-4.0.11.tgz#457d7cdcff1badfc9837441a562957dae7eeecea" + +lockfile@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.2.tgz#97e1990174f696cbe0a3acd58a43b84aa30c7c83" + +lodash._arraycopy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" + +lodash._arrayeach@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._baseclone@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" + dependencies: + lodash._arraycopy "^3.0.0" + lodash._arrayeach "^3.0.0" + lodash._baseassign "^3.0.0" + lodash._basefor "^3.0.0" + lodash.isarray "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._baseflatten@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz#0770ff80131af6e34f3b511796a7ba5214e65ff7" + dependencies: + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash._basefor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._createassigner@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" + dependencies: + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.restparam "^3.0.0" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._reinterpolate@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash.assign@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" + dependencies: + lodash._baseassign "^3.0.0" + lodash._createassigner "^3.0.0" + lodash.keys "^3.0.0" + +lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.assignin@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" + +lodash.clonedeep@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db" + dependencies: + lodash._baseclone "^3.0.0" + lodash._bindcallback "^3.0.0" + +lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.4.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.debounce@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-3.1.1.tgz#812211c378a94cc29d5aa4e3346cf0bfce3a7df5" + dependencies: + lodash._getnative "^3.0.0" + +lodash.find@^4.5.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" + +lodash.flatten@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-3.0.2.tgz#de1cf57758f8f4479319d35c3e9cc60c4501938c" + dependencies: + lodash._baseflatten "^3.0.0" + lodash._isiterateecall "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isplainobject@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz#9a8238ae16b200432960cd7346512d0123fbf4c5" + dependencies: + lodash._basefor "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.keysin "^3.0.0" + +lodash.istypedarray@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.keysin@^3.0.0: + version "3.0.8" + resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-3.0.8.tgz#22c4493ebbedb1427962a54b445b2c8a767fb47f" + dependencies: + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.merge@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-3.3.2.tgz#0d90d93ed637b1878437bb3e21601260d7afe994" + dependencies: + lodash._arraycopy "^3.0.0" + lodash._arrayeach "^3.0.0" + lodash._createassigner "^3.0.0" + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + lodash.isplainobject "^3.0.0" + lodash.istypedarray "^3.0.0" + lodash.keys "^3.0.0" + lodash.keysin "^3.0.0" + lodash.toplainobject "^3.0.0" + +lodash.merge@^4.3.0, lodash.merge@^4.4.0, lodash.merge@^4.5.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" + +lodash.omit@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" + +lodash.pad@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.pad/-/lodash.pad-4.5.1.tgz#4330949a833a7c8da22cc20f6a26c4d59debba70" + +lodash.padend@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + +lodash.padstart@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash.template@^4.2.5: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + dependencies: + lodash._reinterpolate "~3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + dependencies: + lodash._reinterpolate "~3.0.0" + +lodash.toplainobject@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash.toplainobject/-/lodash.toplainobject-3.0.0.tgz#28790ad942d293d78aa663a07ecf7f52ca04198d" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keysin "^3.0.0" + +lodash.uniq@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@3.7.x: + version "3.7.0" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45" + +lodash@^3.10.0, lodash@^3.10.1, lodash@^3.9.3: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +lodash@^4.0.0, lodash@^4.12.0, lodash@^4.14.0, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1: + version "4.17.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42" + +lodash@~4.16.4: + version "4.16.6" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + +lru-cache@^4.0.1, lru-cache@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" + dependencies: + pseudomap "^1.0.1" + yallist "^2.0.0" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +markdown-it-terminal@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/markdown-it-terminal/-/markdown-it-terminal-0.0.3.tgz#c77a8533c2170b46d2a907a3c3452d4d7f4aa5db" + dependencies: + ansi-styles "^2.1.0" + cardinal "^0.5.0" + cli-table "^0.3.1" + lodash.merge "^3.3.2" + markdown-it "^4.4.0" + +markdown-it@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-7.0.0.tgz#d5ae413e160c862246573b0df79025fb9795fcf2" + dependencies: + argparse "^1.0.7" + entities "~1.1.1" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.1" + +markdown-it@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-4.4.0.tgz#3df373dbea587a9a7fef3e56311b68908f75c414" + dependencies: + argparse "~1.0.2" + entities "~1.1.1" + linkify-it "~1.2.0" + mdurl "~1.0.0" + uc.micro "^1.0.0" + +marked-terminal@^1.6.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904" + dependencies: + cardinal "^1.0.0" + chalk "^1.1.3" + cli-table "^0.3.1" + lodash.assign "^4.2.0" + node-emoji "^1.4.1" + +marked@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" + +matcher-collection@^1.0.0, matcher-collection@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-1.0.4.tgz#2f66ae0869996f29e43d0b62c83dd1d43e581755" + dependencies: + minimatch "^3.0.2" + +md5-hex@^1.0.2, md5-hex@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-1.3.0.tgz#d2c4afe983c4370662179b8cad145219135046c4" + dependencies: + md5-o-matic "^0.1.1" + +md5-o-matic@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/md5-o-matic/-/md5-o-matic-0.1.1.tgz#822bccd65e117c514fab176b25945d54100a03c3" + +mdurl@^1.0.1, mdurl@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +memory-streams@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/memory-streams/-/memory-streams-0.1.0.tgz#bec658a71e3f28b0f0c2f1b14501c2db547d5f7a" + dependencies: + readable-stream "~1.0.2" + +meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +merge@^1.1.3, merge@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +"mime-db@>= 1.24.0 < 2", mime-db@~1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" + +mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: + version "2.1.13" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" + dependencies: + mime-db "~1.25.0" + +mime@1.3.4, mime@^1.2.11: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +minimatch@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-1.0.0.tgz#e0dd2120b49e1b724ce8d714c520822a9438576d" + dependencies: + lru-cache "2" + sigmund "~1.0.0" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@~3.0.0, minimatch@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimatch@^2.0.1, minimatch@^2.0.3: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" + dependencies: + brace-expansion "^1.0.0" + +minimist@0.0.8, minimist@~0.0.1: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mkdirp@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" + +mkdirp@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.4.2.tgz#427c8c18ece398b932f6f666f4e1e5b7740e78c8" + dependencies: + minimist "0.0.8" + +mktemp@~0.3.4: + version "0.3.5" + resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.3.5.tgz#a1504c706d0d2b198c6a0eb645f7fdaf8181f7de" + +morgan@^1.5.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.7.0.tgz#eb10ca8e50d1abe0f8d3dad5c0201d052d981c62" + dependencies: + basic-auth "~1.0.3" + debug "~2.2.0" + depd "~1.1.0" + on-finished "~2.3.0" + on-headers "~1.0.1" + +mout@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mout/-/mout-1.0.0.tgz#9bdf1d4af57d66d47cb353a6335a3281098e1501" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +mustache@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +mute-stream@0.0.6, mute-stream@~0.0.4: + version "0.0.6" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" + +nan@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-emoji@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.4.3.tgz#5272f70b823c4df6d7c39f84fd8203f35b3e5d36" + dependencies: + string.prototype.codepointat "^0.2.0" + +node-fetch@^1.3.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-gyp@^3.3.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.4.0.tgz#dda558393b3ecbbe24c9e6b8703c71194c63fa36" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + minimatch "^3.0.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3" + osenv "0" + path-array "^1.0.0" + request "2" + rimraf "2" + semver "2.x || 3.x || 4 || 5" + tar "^2.0.0" + which "1" + +node-gyp@~3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.3.1.tgz#80f7b6d7c2f9c0495ba42c518a670c99bdf6e4a0" + dependencies: + fstream "^1.0.0" + glob "3 || 4" + graceful-fs "^4.1.2" + minimatch "1" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2" + osenv "0" + path-array "^1.0.0" + request "2" + rimraf "2" + semver "2.x || 3.x || 4 || 5" + tar "^2.0.0" + which "1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-modules-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/node-modules-path/-/node-modules-path-1.0.1.tgz#40096b08ce7ad0ea14680863af449c7c75a5d1c8" + +node-notifier@^4.3.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-4.6.1.tgz#056d14244f3dcc1ceadfe68af9cff0c5473a33f3" + dependencies: + cli-usage "^0.1.1" + growly "^1.2.0" + lodash.clonedeep "^3.0.0" + minimist "^1.1.1" + semver "^5.1.0" + shellwords "^0.1.0" + which "^1.0.5" + +node-sass@^3.8.0: + version "3.13.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.13.0.tgz#d08b95bdebf40941571bd2c16a9334b980f8924f" + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.3.2" + node-gyp "^3.3.1" + npmlog "^4.0.0" + request "^2.61.0" + sass-graph "^2.1.1" + +node-uuid@^1.4.3, node-uuid@~1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f" + +"nopt@2 || 3", nopt@^3.0.1, nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +normalize-git-url@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/normalize-git-url/-/normalize-git-url-3.0.2.tgz#8e5f14be0bdaedb73e07200310aa416c27350fc4" + +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, "normalize-package-data@~1.0.1 || ^2.0.0", normalize-package-data@~2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +npm-cache-filename@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz#ded306c5b0bfc870a9e9faf823bc5f283e05ae11" + +npm-git-info@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/npm-git-info/-/npm-git-info-1.0.3.tgz#a933c42ec321e80d3646e0d6e844afe94630e1d5" + +npm-install-checks@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-1.0.7.tgz#6d91aeda0ac96801f1ed7aadee116a6c0a086a57" + dependencies: + npmlog "0.1 || 1 || 2" + semver "^2.3.0 || 3.x || 4 || 5" + +"npm-package-arg@^3.0.0 || ^4.0.0", npm-package-arg@^4.0.0, npm-package-arg@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.2.0.tgz#809bc61cabf54bd5ff94f6165c89ba8ee88c115c" + dependencies: + hosted-git-info "^2.1.5" + semver "^5.1.0" + +npm-package-arg@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.1.1.tgz#86d9dca985b4c5e5d59772dfd5de6919998a495a" + dependencies: + hosted-git-info "^2.1.4" + semver "4 || 5" + +npm-registry-client@~7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-7.1.2.tgz#ddf243a2bd149d35172fe680aff40dfa20054bc3" + dependencies: + chownr "^1.0.1" + concat-stream "^1.4.6" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + normalize-package-data "~1.0.1 || ^2.0.0" + npm-package-arg "^3.0.0 || ^4.0.0" + once "^1.3.0" + request "^2.47.0" + retry "^0.8.0" + rimraf "2" + semver "2 >=2.2.1 || 3.x || 4 || 5" + slide "^1.1.3" + optionalDependencies: + npmlog "~2.0.0 || ~3.1.0" + +npm-user-validate@~0.1.2: + version "0.1.5" + resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-0.1.5.tgz#52465d50c2d20294a57125b996baedbf56c5004b" + +npm@2.15.5: + version "2.15.5" + resolved "https://registry.yarnpkg.com/npm/-/npm-2.15.5.tgz#5fcd71999c3d54baa0e1c27ac44f84a1b82b4559" + dependencies: + abbrev "~1.0.7" + ansi "~0.3.1" + ansicolors "~0.3.2" + ansistyles "~0.1.3" + archy "~1.0.0" + async-some "~1.0.2" + block-stream "0.0.9" + char-spinner "~1.0.1" + chmodr "~1.0.2" + chownr "~1.0.1" + cmd-shim "~2.0.2" + columnify "~1.5.4" + config-chain "~1.1.10" + dezalgo "~1.0.3" + editor "~1.0.0" + fs-vacuum "~1.2.9" + fs-write-stream-atomic "~1.0.8" + fstream "~1.0.8" + fstream-npm "~1.0.7" + github-url-from-git "~1.4.0" + github-url-from-username-repo "~1.0.2" + glob "~7.0.3" + graceful-fs "~4.1.4" + hosted-git-info "~2.1.4" + inflight "~1.0.4" + inherits "~2.0.1" + ini "~1.3.4" + init-package-json "~1.9.3" + lockfile "~1.0.1" + lru-cache "~4.0.1" + minimatch "~3.0.0" + mkdirp "~0.5.1" + node-gyp "~3.3.1" + nopt "~3.0.6" + normalize-git-url "~3.0.2" + normalize-package-data "~2.3.5" + npm-cache-filename "~1.0.2" + npm-install-checks "~1.0.7" + npm-package-arg "~4.1.0" + npm-registry-client "~7.1.0" + npm-user-validate "~0.1.2" + npmlog "~2.0.3" + once "~1.3.3" + opener "~1.4.1" + osenv "~0.1.3" + path-is-inside "~1.0.0" + read "~1.0.7" + read-installed "~4.0.3" + read-package-json "~2.0.4" + readable-stream "~2.1.2" + realize-package-specifier "~3.0.3" + request "~2.72.0" + retry "~0.9.0" + rimraf "~2.5.2" + semver "~5.1.0" + sha "~2.0.1" + slide "~1.1.6" + sorted-object "~2.0.0" + spdx-license-ids "~1.2.1" + strip-ansi "~3.0.1" + tar "~2.2.1" + text-table "~0.2.0" + uid-number "0.0.6" + umask "~1.1.0" + validate-npm-package-license "~3.0.1" + validate-npm-package-name "~2.2.2" + which "~1.2.8" + wrappy "~1.0.1" + write-file-atomic "~1.1.4" + +"npmlog@0 || 1 || 2", "npmlog@0.1 || 1 || 2", npmlog@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-2.0.4.tgz#98b52530f2514ca90d09ec5b22c8846722375692" + dependencies: + ansi "~0.3.1" + are-we-there-yet "~1.1.2" + gauge "~1.2.5" + +"npmlog@0 || 1 || 2 || 3", "npmlog@~2.0.0 || ~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-3.1.2.tgz#2d46fa874337af9498a2f12bb43d8d0be4a36873" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.6.0" + set-blocking "~2.0.0" + +npmlog@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.1.tgz#d14f503b4cd79710375553004ba96e6662fbc0b8" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.1" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +numeral@^1.5.3: + version "1.5.6" + resolved "https://registry.yarnpkg.com/numeral/-/numeral-1.5.6.tgz#3831db968451b9cf6aff9bf95925f1ef8e37b33f" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +once@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +opener@~1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.2.tgz#b32582080042af8680c389a499175b4c54fff523" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +ora@^0.2.0: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@0, osenv@^0.1.0, osenv@^0.1.3, osenv@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.3.tgz#83cf05c6d6458fc4d5ac6362ea325d92f2754217" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +output-file-sync@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" + dependencies: + graceful-fs "^4.1.4" + mkdirp "^0.5.1" + object-assign "^4.1.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parsejson@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.1.tgz#9b10c6c0d825ab589e685153826de0a3ba278bcc" + dependencies: + better-assert "~1.0.0" + +parseqs@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.2.tgz#9dfe70b2cddac388bde4f35b1f240fa58adbe6c7" + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.4.tgz#806582a39887e1ea18dd5e2fe0e01902268e9350" + dependencies: + better-assert "~1.0.0" + +parseurl@~1.3.0, parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-array@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-array/-/path-array-1.0.1.tgz#7e2f0f35f07a2015122b868b7eac0eb2c4fec271" + dependencies: + array-index "^1.0.0" + +path-exists@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-posix@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +portfinder@^1.0.4: + version "1.0.10" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.10.tgz#7a4de9d98553c315da6f1e1ed05138eeb2d16bb8" + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +printf@^0.2.3: + version "0.2.5" + resolved "https://registry.yarnpkg.com/printf/-/printf-0.2.5.tgz#c438ca2ca33e3927671db4ab69c0e52f936a4f0f" + +private@^0.1.6, private@~0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process-relative-require@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/process-relative-require/-/process-relative-require-1.0.0.tgz#1590dfcf5b8f2983ba53e398446b68240b4cc68a" + dependencies: + node-modules-path "^1.0.0" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +promise-map-series@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/promise-map-series/-/promise-map-series-0.2.3.tgz#c2d377afc93253f6bd03dbb77755eb88ab20a847" + dependencies: + rsvp "^3.0.14" + +promzard@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" + dependencies: + read "1" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + +proxy-addr@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.1.1" + +pseudomap@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" + +qs@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.0.tgz#a9f31142af468cb72b25b30136ba2456834916be" + +qs@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" + +qs@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-5.1.0.tgz#4d932e5c7ea411cca76a312d39a606200fd50cd9" + +qs@~6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.1.0.tgz#ec1d1626b24278d99f0fdf4549e524e24eceeb26" + +qs@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" + +quick-temp@0.1.5, quick-temp@^0.1.0, quick-temp@^0.1.2, quick-temp@^0.1.3, quick-temp@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/quick-temp/-/quick-temp-0.1.5.tgz#0d0d67f0fb6a589a0e142f90985f76cdbaf403f7" + dependencies: + mktemp "~0.3.4" + rimraf "~2.2.6" + underscore.string "~2.3.3" + +qunitjs@^1.20.0: + version "1.23.1" + resolved "https://registry.yarnpkg.com/qunitjs/-/qunitjs-1.23.1.tgz#1971cf97ac9be01a64d2315508d2e48e6fd4e719" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@~2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.13" + unpipe "1.0.0" + +read-installed@~4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" + dependencies: + debuglog "^1.0.1" + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + semver "2 || 3 || 4 || 5" + slide "~1.1.3" + util-extend "^1.0.1" + optionalDependencies: + graceful-fs "^4.1.2" + +"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.4.tgz#61ed1b2256ea438d8008895090be84b8e799c853" + dependencies: + glob "^6.0.0" + json-parse-helpfulerror "^1.0.2" + normalize-package-data "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.2" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read@1, read@~1.0.1, read@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + dependencies: + mute-stream "~0.0.4" + +"readable-stream@1 || 2", readable-stream@^2.0.2, readable-stream@~2.1.2: + version "2.1.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@1.1: + version "1.1.13" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@~2.0.0, readable-stream@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@~1.0.2: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readdir-scoped-modules@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +realize-package-specifier@~3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/realize-package-specifier/-/realize-package-specifier-3.0.3.tgz#d0def882952b8de3f67eba5e91199661271f41f4" + dependencies: + dezalgo "^1.0.1" + npm-package-arg "^4.1.1" + +recast@0.10.33: + version "0.10.33" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.33.tgz#942808f7aa016f1fa7142c461d7e5704aaa8d697" + dependencies: + ast-types "0.8.12" + esprima-fb "~15001.1001.0-dev-harmony-fb" + private "~0.1.5" + source-map "~0.5.0" + +recast@^0.10.10: + version "0.10.43" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.43.tgz#b95d50f6d60761a5f6252e15d80678168491ce7f" + dependencies: + ast-types "0.8.15" + esprima-fb "~15001.1001.0-dev-harmony-fb" + private "~0.1.5" + source-map "~0.5.0" + +recast@^0.11.17, recast@^0.11.3: + version "0.11.18" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.18.tgz#07af6257ca769868815209401d4d60eef1b5b947" + dependencies: + ast-types "0.9.2" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redeyed@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-0.5.0.tgz#7ab000e60ee3875ac115d29edb32c1403c6c25d1" + dependencies: + esprima-fb "~12001.1.0-dev-harmony-fb" + +redeyed@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" + dependencies: + esprima "~3.0.0" + +regenerate@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" + +regenerator@0.8.40: + version "0.8.40" + resolved "https://registry.yarnpkg.com/regenerator/-/regenerator-0.8.40.tgz#a0e457c58ebdbae575c9f8cd75127e93756435d8" + dependencies: + commoner "~0.10.3" + defs "~1.1.0" + esprima-fb "~15001.1001.0-dev-harmony-fb" + private "~0.1.5" + recast "0.10.33" + through "~2.3.8" + +regexpu@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexpu/-/regexpu-1.3.0.tgz#e534dc991a9e5846050c98de6d7dd4a55c9ea16d" + dependencies: + esprima "^2.6.0" + recast "^0.10.10" + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^1.1.0, repeating@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + dependencies: + is-finite "^1.0.0" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@2, request@^2.27.0, request@^2.47.0, request@^2.61.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +request@~2.72.0: + version "2.72.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.72.0.tgz#0ce3a179512620b10441f14c82e21c12c0ddb4e1" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + bl "~1.1.2" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~1.0.0-rc3" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.1" + qs "~6.1.0" + stringstream "~0.0.4" + tough-cookie "~2.2.0" + tunnel-agent "~0.4.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +requires-port@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve@^1.1.2, resolve@^1.1.6, resolve@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +retry@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.8.0.tgz#2367628dc0edb247b1eab649dc53ac8628ac2d5f" + +retry@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.9.0.tgz#6f697e50a0e4ddc8c8f7fb547a9b60dead43678d" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.3.2, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.5.2, rimraf@^2.5.3, rimraf@~2.5.2: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + +rimraf@~2.2.6: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + +rsvp@^3.0.14, rsvp@^3.0.16, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0, rsvp@^3.2.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.3.3.tgz#34633caaf8bc66ceff4be3c2e1dffd032538a813" + +rsvp@~3.0.6: + version "3.0.21" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.0.21.tgz#49c588fe18ef293bcd0ab9f4e6756e6ac433359f" + +rsvp@~3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + +sane@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sane/-/sane-1.4.1.tgz#88f763d74040f5f0c256b6163db399bf110ac715" + dependencies: + exec-sh "^0.2.0" + fb-watchman "^1.8.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.10.0" + +sass-graph@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.1.2.tgz#965104be23e8103cb7e5f710df65935b317da57b" + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + yargs "^4.7.1" + +"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.1.0, semver@^5.1.1: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +semver@^4.1.0, semver@^4.2.2: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + +semver@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" + +send@0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.5.0" + mime "1.3.4" + ms "0.7.1" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.0" + +serve-static@~1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.1.tgz#d6cce7693505f733c759de57befc1af76c0f0805" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.14.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +setprototypeof@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" + +sha@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/sha/-/sha-2.0.1.tgz#6030822fbd2c9823949f8f72ed6411ee5cf25aae" + dependencies: + graceful-fs "^4.1.2" + readable-stream "^2.0.2" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shelljs@0.3.x: + version "0.3.0" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" + +shelljs@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.5.tgz#2eef7a50a21e1ccf37da00df767ec69e30ad0675" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shellwords@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.0.tgz#66afd47b6a12932d9071cbfd98a52e785cd0ba14" + +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + +signal-exit@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81" + +silent-error@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/silent-error/-/silent-error-1.0.1.tgz#71b7d503d1c6f94882b51b56be879b113cb4822c" + dependencies: + debug "^2.2.0" + +simple-fmt@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/simple-fmt/-/simple-fmt-0.1.0.tgz#191bf566a59e6530482cb25ab53b4a8dc85c3a6b" + +simple-is@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +slide@^1.1.3, slide@^1.1.5, slide@~1.1.3, slide@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +socket.io-adapter@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.4.0.tgz#fb9f82ab1aa65290bf72c3657955b930a991a24f" + dependencies: + debug "2.2.0" + socket.io-parser "2.2.2" + +socket.io-client@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.5.0.tgz#08232d0adb5a665a7c24bd9796557a33f58f38ae" + dependencies: + backo2 "1.0.2" + component-bind "1.0.0" + component-emitter "1.2.0" + debug "2.2.0" + engine.io-client "1.7.0" + has-binary "0.1.7" + indexof "0.0.1" + object-component "0.0.3" + parseuri "0.0.4" + socket.io-parser "2.2.6" + to-array "0.1.4" + +socket.io-parser@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.2.2.tgz#3d7af6b64497e956b7d9fe775f999716027f9417" + dependencies: + benchmark "1.0.0" + component-emitter "1.1.2" + debug "0.7.4" + isarray "0.0.1" + json3 "3.2.6" + +socket.io-parser@2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.2.6.tgz#38dfd61df50dcf8ab1d9e2091322bf902ba28b99" + dependencies: + benchmark "1.0.0" + component-emitter "1.1.2" + debug "2.2.0" + isarray "0.0.1" + json3 "3.3.2" + +socket.io@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.5.0.tgz#024dd9719d9267d6a6984eebe2ab5ceb9a0b8a98" + dependencies: + debug "2.2.0" + engine.io "1.7.0" + has-binary "0.1.7" + socket.io-adapter "0.4.0" + socket.io-client "1.5.0" + socket.io-parser "2.2.6" + +sorted-object@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/sorted-object/-/sorted-object-2.0.1.tgz#7d631f4bd3a798a24af1dffcfbfe83337a5df5fc" + +source-map-support@^0.2.10: + version "0.2.10" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.2.10.tgz#ea5a3900a1c1cb25096a0ae8cc5c2b4b10ded3dc" + dependencies: + source-map "0.1.32" + +source-map-url@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" + +source-map@0.1.32: + version "0.1.32" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.32.tgz#c8b6c167797ba4740a8ea33252162ff08591b266" + dependencies: + amdefine ">=0.0.4" + +source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@~0.5.0, source-map@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +spawn-args@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/spawn-args/-/spawn-args-0.2.0.tgz#fb7d0bd1d70fd4316bd9e3dec389e65f9d6361bb" + +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2, spdx-license-ids@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sri-toolbox@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sri-toolbox/-/sri-toolbox-0.2.0.tgz#a7fea5c3fde55e675cf1c8c06f3ebb5c2935835e" + +sshpk@^1.7.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stable@~0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.5.tgz#08232f60c732e9890784b5bed0734f8b32a887b9" + +statuses@1, "statuses@>= 1.3.1 < 2", statuses@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + +string.prototype.codepointat@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringmap@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stringmap/-/stringmap-0.2.2.tgz#556c137b258f942b8776f5b2ef582aa069d7d1b1" + +stringset@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/stringset/-/stringset-0.2.1.tgz#ef259c4e349344377fcd1c913dd2e848c9c042b5" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" + dependencies: + ansi-regex "^0.2.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1, strip-ansi@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@1.0.x, strip-json-comments@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + +styled_string@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/styled_string/-/styled_string-0.0.1.tgz#d22782bd81295459bc4f1df18c4bad8e94dd124a" + +sum-up@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sum-up/-/sum-up-1.0.3.tgz#1c661f667057f63bcb7875aa1438bc162525156e" + dependencies: + chalk "^1.0.0" + +supports-color@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +symlink-or-copy@^1.0.0, symlink-or-copy@^1.0.1, symlink-or-copy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.1.8.tgz#cabe61e0010c1c023c173b25ee5108b37f4b4aa3" + +sync-exec@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/sync-exec/-/sync-exec-0.6.2.tgz#717d22cc53f0ce1def5594362f3a89a2ebb91105" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tap-parser@^1.1.3: + version "1.3.2" + resolved "https://registry.yarnpkg.com/tap-parser/-/tap-parser-1.3.2.tgz#120c5089c88c3c8a793ef288867de321e18f8c22" + dependencies: + events-to-array "^1.0.1" + inherits "~2.0.1" + js-yaml "^3.2.7" + optionalDependencies: + readable-stream "^2" + +tar@^2.0.0, tar@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +temp@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + dependencies: + os-tmpdir "^1.0.0" + rimraf "~2.2.6" + +testem@^1.8.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/testem/-/testem-1.13.0.tgz#441779b340afae4bd318d5c2be29b99d8964947f" + dependencies: + backbone "^1.1.2" + bluebird "^3.4.6" + charm "^1.0.0" + commander "^2.6.0" + consolidate "^0.14.0" + cross-spawn "^4.0.0" + did_it_work "0.0.6" + express "^4.10.7" + fireworm "^0.7.0" + glob "^7.0.4" + http-proxy "^1.13.1" + js-yaml "^3.2.5" + lodash.assignin "^4.1.0" + lodash.clonedeep "^4.4.1" + lodash.find "^4.5.1" + mkdirp "^0.5.1" + mustache "^2.2.1" + node-notifier "^4.3.1" + npmlog "^4.0.0" + printf "^0.2.3" + rimraf "^2.4.4" + socket.io "1.5.0" + spawn-args "^0.2.0" + styled_string "0.0.1" + tap-parser "^1.1.3" + xmldom "^0.1.19" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +"textextensions@1 || 2": + version "2.0.1" + resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.0.1.tgz#be8cf22d65379c151319f88f0335ad8f667abdca" + +through@^2.3.6, through@~2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tiny-lr@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-0.2.1.tgz#b3fdba802e5d56a33c2f6f10794b32e477ac729d" + dependencies: + body-parser "~1.14.0" + debug "~2.2.0" + faye-websocket "~0.10.0" + livereload-js "^2.2.0" + parseurl "~1.3.0" + qs "~5.1.0" + +tmp@0.0.28: + version "0.0.28" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" + dependencies: + os-tmpdir "~1.0.1" + +tmp@^0.0.29: + version "0.0.29" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" + dependencies: + os-tmpdir "~1.0.1" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + +to-fast-properties@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" + +tough-cookie@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.2.2.tgz#c83a1830f4e5ef0b93ef2a3488e724f8de016ac7" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tree-sync@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/tree-sync/-/tree-sync-1.1.5.tgz#3ccb5c7c0bfaf1fab4dd89dd48206ab2b413be72" + dependencies: + debug "^2.2.0" + fs-tree-diff "^0.5.2" + mkdirp "^0.5.1" + quick-temp "^0.1.5" + walk-sync "^0.2.7" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-right@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +try-resolve@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/try-resolve/-/try-resolve-1.0.1.tgz#cfde6fabd72d63e5797cfaab873abbe8e700e912" + +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + +tryor@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.4" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.4.tgz#8c9dbfb52795686f166cd2023794bcf103d13c2b" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.10, type-is@~1.6.13: + version "1.6.14" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.13" + +typedarray@~0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uc.micro@^1.0.0, uc.micro@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" + +uglify-js@^2.6, uglify-js@^2.6.0: + version "2.7.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8" + dependencies: + async "~0.2.6" + source-map "~0.5.1" + uglify-to-browserify "~1.0.0" + yargs "~3.10.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uid-number@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +ultron@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +umask@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" + +underscore.string@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d" + +underscore@>=1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +untildify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" + dependencies: + os-homedir "^1.0.0" + +user-home@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util-extend@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + +uuid@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" + +validate-npm-package-license@^3.0.1, validate-npm-package-license@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +validate-npm-package-name@^2.0.1, validate-npm-package-name@~2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-2.2.2.tgz#f65695b22f7324442019a3c7fa39a6e7fd299085" + dependencies: + builtins "0.0.7" + +vary@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +walk-sync@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.1.3.tgz#8a07261a00bda6cfb1be25e9f100fad57546f583" + +walk-sync@^0.2.5, walk-sync@^0.2.6, walk-sync@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.2.7.tgz#b49be4ee6867657aeb736978b56a29d10fa39969" + dependencies: + ensure-posix-path "^1.0.0" + matcher-collection "^1.0.0" + +walk-sync@^0.3.0, walk-sync@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.1.tgz#558a16aeac8c0db59c028b73c66f397684ece465" + dependencies: + ensure-posix-path "^1.0.0" + matcher-collection "^1.0.0" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + dependencies: + makeerror "1.0.x" + +watch@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" + +wcwidth@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + dependencies: + defaults "^1.0.3" + +websocket-driver@>=0.5.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + dependencies: + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which@1, which@^1.0.5, which@^1.2.9, which@~1.2.8: + version "1.2.12" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" + dependencies: + isexe "^1.1.1" + +wide-align@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" + dependencies: + string-width "^1.0.1" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +window-size@^0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + +window-size@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1, wrappy@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.2.0.tgz#14c66d4e4cb3ca0565c28cf3b7a6f3e4d5938fab" + dependencies: + graceful-fs "^4.1.2" + imurmurhash "^0.1.4" + slide "^1.1.5" + +write-file-atomic@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.1.4.tgz#b1f52dc2e8dc0e3cb04d187a25f758a38a90ca3b" + dependencies: + graceful-fs "^4.1.2" + imurmurhash "^0.1.4" + slide "^1.1.5" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +ws@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + +wtf-8@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a" + +xdg-basedir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" + dependencies: + os-homedir "^1.0.0" + +xmldom@^0.1.19: + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xmlhttprequest-ssl@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.1.tgz#3b7741fea4a86675976e908d296d4445961faa67" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.0, y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.0.0.tgz#306c543835f09ee1a4cb23b7bce9ab341c91cdd4" + +yam@0.0.21: + version "0.0.21" + resolved "https://registry.yarnpkg.com/yam/-/yam-0.0.21.tgz#2b74a3c973193d58bd13cb5018c0245f82c8c6d3" + dependencies: + findup "^0.1.5" + fs-extra "^0.30.0" + lodash.merge "^4.4.0" + +yargs-parser@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4" + dependencies: + camelcase "^3.0.0" + lodash.assign "^4.0.6" + +yargs@^4.7.1: + version "4.8.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0" + dependencies: + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + lodash.assign "^4.0.3" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.1" + which-module "^1.0.0" + window-size "^0.2.0" + y18n "^3.2.1" + yargs-parser "^2.4.1" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" + +yargs@~3.27.0: + version "3.27.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.27.0.tgz#21205469316e939131d59f2da0c6d7f98221ea40" + dependencies: + camelcase "^1.2.1" + cliui "^2.1.0" + decamelize "^1.0.0" + os-locale "^1.4.0" + window-size "^0.1.2" + y18n "^3.2.0" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" diff --git a/src/api-umbrella/cli/read_config.lua b/src/api-umbrella/cli/read_config.lua index 781877359..1d5a2d672 100644 --- a/src/api-umbrella/cli/read_config.lua +++ b/src/api-umbrella/cli/read_config.lua @@ -379,6 +379,10 @@ local function set_computed_config() }, }) + if config["app_env"] == "development" then + config["_dev_env_install_dir"] = path.join(src_root_dir, "build/work/dev-env") + end + if config["app_env"] == "test" then config["_test_env_install_dir"] = path.join(src_root_dir, "build/work/test-env") end diff --git a/src/api-umbrella/cli/reload.lua b/src/api-umbrella/cli/reload.lua index 3640ec216..d21b84998 100644 --- a/src/api-umbrella/cli/reload.lua +++ b/src/api-umbrella/cli/reload.lua @@ -46,6 +46,14 @@ local function reload_nginx(perp_base) end end +local function reload_dev_env_ember_server(perp_base) + local _, _, err = run_command("perpctl -b " .. perp_base .. " term dev-env-ember-server") + if err then + print("Failed to reload dev-env-ember-server\n" .. err) + os.exit(1) + end +end + local function reload_nginx_reloader(perp_base) local _, _, err = run_command("perpctl -b " .. perp_base .. " term nginx-reloader") if err then @@ -81,4 +89,8 @@ return function(options) reload_nginx_reloader(perp_base) end end + + if config["app_env"] == "development" then + reload_dev_env_ember_server(perp_base) + end end diff --git a/src/api-umbrella/cli/setup.lua b/src/api-umbrella/cli/setup.lua index ced26df10..3fc46afac 100644 --- a/src/api-umbrella/cli/setup.lua +++ b/src/api-umbrella/cli/setup.lua @@ -101,7 +101,6 @@ local function prepare() path.join(config["etc_dir"], "trafficserver/snapshots"), path.join(config["log_dir"], "trafficserver"), path.join(config["root_dir"], "var/trafficserver"), - path.join(config["_src_root_dir"], "src/api-umbrella/web-app/tmp"), } for _, directory in ipairs(dirs) do @@ -154,10 +153,23 @@ local function write_templates() for _, filename in ipairs(files) do local template_path = path.join(root, filename) + local process = true local is_hidden = (string.find(filename, ".", 1, true) == 1) + if is_hidden then + process = false + end + + local is_dev_file = (string.find(template_path, "dev-env") ~= nil) + if is_dev_file and config["app_env"] ~= "development" then + process = false + end + local is_test_file = (string.find(template_path, "test-env") ~= nil) + if is_test_file and config["app_env"] ~= "test" then + process = false + end - if not is_hidden and (not is_test_file or config["app_env"] == "test") then + if process then local install_path = string.gsub(template_path, "^" .. plutils.escape(template_root .. "/"), "", 1) install_path = string.gsub(install_path, plutils.escape(".mustache") .. "$", "", 1) install_path = path.join(config["etc_dir"], install_path) @@ -204,12 +216,6 @@ local function set_permissions() os.exit(1) end - _, _, err = run_command("chmod 1777 " .. path.join(config["_src_root_dir"], "src/api-umbrella/web-app/tmp")) - if err then - print("chmod failed: ", err) - os.exit(1) - end - if config["user"] and config["group"] then _, _, err = run_command("chown -R " .. config["user"] .. ":" .. config["group"] .. " " .. path.join(config["etc_dir"], "trafficserver") .. " " .. path.join(config["root_dir"], "var")) if err then @@ -274,6 +280,15 @@ local function activate_services() end end + -- Disable any dev-only services when not running in the dev environment. + if string.find(service_name, "dev-env", 1, true) == 1 then + if config["app_env"] == "development" then + is_active = true + else + is_active = false + end + end + -- Disable any test-only services when not running in the test environment. if string.find(service_name, "test-env", 1, true) == 1 then if config["app_env"] == "test" then diff --git a/src/api-umbrella/http-api/health.lua b/src/api-umbrella/http-api/health.lua index 9e23535f4..2cbd4ef25 100644 --- a/src/api-umbrella/http-api/health.lua +++ b/src/api-umbrella/http-api/health.lua @@ -49,7 +49,7 @@ local function status_response() end end - res, err = httpc:request_uri("http://127.0.0.1:" .. config["web"]["port"] .. "/admin/") + res, err = httpc:request_uri("http://127.0.0.1:" .. config["web"]["port"] .. "/_web-app-health") if err then ngx.log(ngx.ERR, "failed to fetch web app: ", err) elseif res.body then diff --git a/src/api-umbrella/proxy/hooks/api_backends_access.lua b/src/api-umbrella/proxy/hooks/api_backends_access.lua index ea3992d4b..71d9dfc8e 100644 --- a/src/api-umbrella/proxy/hooks/api_backends_access.lua +++ b/src/api-umbrella/proxy/hooks/api_backends_access.lua @@ -1,2 +1,5 @@ local wait_for_setup = require "api-umbrella.proxy.wait_for_setup" wait_for_setup() + +local log_timing_id = ngx.var.x_api_umbrella_request_id .. "_upstream_response_time" +ngx.shared.logs:set(log_timing_id, "pending", 300) diff --git a/src/api-umbrella/proxy/hooks/log_api_backend_proxy.lua b/src/api-umbrella/proxy/hooks/log_api_backend_proxy.lua index 71aa0795e..471457b64 100644 --- a/src/api-umbrella/proxy/hooks/log_api_backend_proxy.lua +++ b/src/api-umbrella/proxy/hooks/log_api_backend_proxy.lua @@ -8,5 +8,21 @@ local ngx_var = ngx.var local log_timing_id = ngx_var.x_api_umbrella_request_id .. "_upstream_response_time" local upstream_response_time = tonumber(ngx_var.upstream_response_time) if upstream_response_time then - ngx.shared.logs:set(log_timing_id, upstream_response_time, 60) + if config["app_env"] == "test" and ngx.var.http_x_api_umbrella_test_simulate_out_of_order_logging == "true" then + -- For the test environment, simulate the rare case where the initial + -- proxy's logging occurs before this backend proxy's logging. + -- + -- This is important to test, since log_initial_proxy.lua's behavior + -- changes when this edge case is hit, and we continue logging inside a + -- timer callback. Since not all nginx variables are available in the timer + -- context, we want to make sure we can reliably test this scenario and + -- ensure that code-path works (rather than it being rare and hard to + -- reproduce in the test suite). + local function set_fake_delayed_response_time() + ngx.shared.logs:set(log_timing_id, 99, 60) + end + ngx.timer.at(0.2, set_fake_delayed_response_time) + else + ngx.shared.logs:set(log_timing_id, upstream_response_time, 60) + end end diff --git a/src/api-umbrella/proxy/hooks/log_initial_proxy.lua b/src/api-umbrella/proxy/hooks/log_initial_proxy.lua index da85e5bb7..4fb9d9db2 100644 --- a/src/api-umbrella/proxy/hooks/log_initial_proxy.lua +++ b/src/api-umbrella/proxy/hooks/log_initial_proxy.lua @@ -79,7 +79,32 @@ local function cache_new_city_geocode(data) end end -local function log_request() +local function log_request(data) + local syslog_message = "<" .. syslog_priority .. ">" + .. syslog_version + .. " " .. os.date("!%Y-%m-%dT%TZ", data["timestamp_utc"] / 1000) -- timestamp + .. " -" -- hostname + .. " api-umbrella" -- app-name + .. " -" -- procid + .. " -" -- msgid + .. " -" -- structured-data + .. " @cee:" -- CEE-enhanced logging for rsyslog to parse JSON + .. elasticsearch_encode_json(data) -- JSON data + .. "\n" + + -- Check the syslog message length to ensure it doesn't exceed the configured + -- rsyslog maxMessageSize value. + -- + -- In general, this shouldn't be possible, since URLs can't exceed 8KB, and + -- we truncate the various headers that users can control for logging + -- purposes. However, this provides an extra sanity check to ensure this + -- doesn't unexpectedly pop up (eg, if we add additional headers we forget to + -- truncate). + local syslog_message_length = string.len(syslog_message) + if syslog_message_length > 32000 then + ngx.log(ngx.ERR, "request syslog message longer than expected - analytics logging may fail: ", syslog_message_length) + end + -- Init the resty logger socket. if not logger.initted() then local ok, err = logger.init{ @@ -96,6 +121,81 @@ local function log_request() end end + local _, err = logger.log(syslog_message) + if err then + ngx.log(ngx.ERR, "failed to log message: ", err) + return + end + + if data["timer_backend_response"] then + local log_timing_id = data["id"] .. "_upstream_response_time" + ngx.shared.logs:delete(log_timing_id) + end + + if data["request_ip_lat"] then + cache_new_city_geocode(data) + end +end + +local function combine_log_data(premature, data) + if premature then + return + end + + -- If we are combining data after waiting for the backend data become + -- populated (this should be rare), then check for the timer information + -- again from the backend. + if data["_timer_backend_response"] == "pending" then + local log_timing_id = data["id"] .. "_upstream_response_time" + data["_timer_backend_response"] = ngx.shared.logs:get(log_timing_id) + end + + -- Pop the temporary variables off the data table. + local timer_backend_response = data["_timer_backend_response"] + data["_timer_backend_response"] = nil + local upstream_response_time = data["_upstream_response_time"] + data["_upstream_response_time"] = nil + + -- If we have more accurate timing information from the API backend layer, + -- then calculate additional timings. + if type(timer_backend_response) == "number" then + data["timer_backend_response"] = timer_backend_response + + -- Try to determine the overhead API Umbrella incurred on the request. + -- First we compare the upstream times from this initial proxy to the + -- backend api router proxy. Note that we don't use the "request_time" + -- variables, since that could be affected by slow clients. + data["timer_proxy_overhead"] = (tonumber(upstream_response_time) or 0) - timer_backend_response + + -- Since we're using the upstream response times for determining overhead, + -- next add in the amount of time we've calculated that we've used + -- internally in the Lua code. + -- + -- Note: Due to how openresty caches the ngx.now() calls (unless we call + -- ngx.update_time, which we don't want to do on every request), this timer + -- will be very approximate, but we mainly want this for detecting if + -- things really start to increase dramatically. + if data["timer_internal"] then + data["timer_proxy_overhead"] = data["timer_proxy_overhead"] + data["timer_internal"] + end + end + + -- Turn any internal fields from seconds (with millisecond precision + -- decimals) into milliseconds. + for _, msec_field in ipairs(log_utils.MSEC_FIELDS) do + if data[msec_field] then + -- Round the results after turning into milliseconds. Since all the nginx + -- timers only have millisecond precision, any decimals left after + -- converting are just an artifact of the original float storage or math + -- (eg, 1.00001... or 1.999988..). + data[msec_field] = utils.round(data[msec_field] * 1000) + end + end + + log_request(data) +end + +local function build_log_data() -- Fetch all the request and response headers. local request_headers = flatten_headers(ngx.req.get_headers()); local response_headers = flatten_headers(ngx.resp.get_headers()); @@ -177,51 +277,24 @@ local function log_request() data["timestamp_tz_hour"] = string.sub(tz_time, 1, 13) .. ":00:00" -- YYYY-MM-DD HH:00:00 data["timestamp_tz_minute"] = tz_time -- YYYY-MM-DD HH:MM:00 - -- Check for log data set by the separate api backend proxy - -- (log_api_backend_proxy.lua). This is used for timing information. - local log_timing_id = id .. "_upstream_response_time" - local timer_backend_response = ngx.shared.logs:get(log_timing_id) - if timer_backend_response then - data["timer_backend_response"] = timer_backend_response - - -- Try to determine the overhead API Umbrella incurred on the request. - -- First we compare the upstream times from this initial proxy to the - -- backend api router proxy. Note that we don't use the "request_time" - -- variables, since that could be affected by slow clients. - data["timer_proxy_overhead"] = (tonumber(ngx_var.upstream_response_time) or 0) - timer_backend_response - - -- Since we're using the upstream response times for determining overhead, - -- next add in the amount of time we've calculated that we've used - -- internally in the Lua code. - -- - -- Note: Due to how openresty caches the ngx.now() calls (unless we call - -- ngx.update_time, which we don't want to do on every request), this timer - -- will be very approximate, but we mainly want this for detecting if - -- things really start to increase dramatically. - if data["timer_internal"] then - data["timer_proxy_overhead"] = data["timer_proxy_overhead"] + data["timer_internal"] - end - end + local geoip_latitude = ngx_var.geoip_latitude + if geoip_latitude then + data["request_ip_lat"] = tonumber(geoip_latitude) + data["request_ip_lon"] = tonumber(ngx_var.geoip_longitude) - if not data["timer_proxy_overhead"] then - data["timer_proxy_overhead"] = ngx_ctx.internal_overhead + data["legacy_request_ip_location"] = { + lat = data["request_ip_lat"], + lon = data["request_ip_lon"], + } end - -- Turn any internal fields from seconds (with millisecond precision - -- decimals) into milliseconds. - for _, msec_field in ipairs(log_utils.MSEC_FIELDS) do - if data[msec_field] then - -- Round the results after turning into milliseconds. Since all the nginx - -- timers only have millisecond precision, any decimals left after - -- converting are just an artifact of the original float storage or math - -- (eg, 1.00001... or 1.999988..). - data[msec_field] = utils.round(data[msec_field] * 1000) - end + -- The geoip database returns "00" for unknown regions sometimes: + -- http://maxmind.com/download/geoip/kml/index.html Remove these and treat + -- these as nil. + if data["request_ip_region"] == "00" then + data["request_ip_region"] = nil end - -- Set the various URL fields. - log_utils.set_url_fields(data) - if request_headers["user-agent"] then local user_agent_data = user_agent_parser(request_headers["user-agent"]) if user_agent_data then @@ -230,65 +303,38 @@ local function log_request() end end - -- The geoip database returns "00" for unknown regions sometimes: - -- http://maxmind.com/download/geoip/kml/index.html Remove these and treat - -- these as nil. - if data["request_ip_region"] == "00" then - data["request_ip_region"] = nil - end - - local geoip_latitude = ngx_var.geoip_latitude - if geoip_latitude then - data["request_ip_lat"] = tonumber(geoip_latitude) - data["request_ip_lon"] = tonumber(ngx_var.geoip_longitude) + -- Set the various URL fields. + log_utils.set_url_fields(data) - data["legacy_request_ip_location"] = { - lat = data["request_ip_lat"], - lon = data["request_ip_lon"], - } - end + -- Set the default timer_proxy_overhead. This may be overwritten by a more + -- accurate number in combine_log_data(). + data["timer_proxy_overhead"] = ngx_ctx.internal_overhead - local syslog_message = "<" .. syslog_priority .. ">" - .. syslog_version - .. " " .. os.date("!%Y-%m-%dT%TZ", data["timestamp_utc"] / 1000) -- timestamp - .. " -" -- hostname - .. " api-umbrella" -- app-name - .. " -" -- procid - .. " -" -- msgid - .. " -" -- structured-data - .. " @cee:" -- CEE-enhanced logging for rsyslog to parse JSON - .. elasticsearch_encode_json(data) -- JSON data - .. "\n" + -- Grab the upstream_respone_time for temporary use. + data["_upstream_response_time"] = ngx_var.upstream_response_time - -- Check the syslog message length to ensure it doesn't exceed the configured - -- rsyslog maxMessageSize value. + -- Check for log data set by the separate api backend proxy + -- (log_api_backend_proxy.lua). This is used for timing information. -- - -- In general, this shouldn't be possible, since URLs can't exceed 8KB, and - -- we truncate the various headers that users can control for logging - -- purposes. However, this provides an extra sanity check to ensure this - -- doesn't unexpectedly pop up (eg, if we add additional headers we forget to - -- truncate). - local syslog_message_length = string.len(syslog_message) - if syslog_message_length > 32000 then - ngx.log(ngx.ERR, "request syslog message longer than expected - analytics logging may fail: ", syslog_message_length) - end - - local _, err = logger.log(syslog_message) - if err then - ngx.log(ngx.ERR, "failed to log message: ", err) - return - end - - if timer_backend_response then - ngx.shared.logs:delete(log_timing_id) - end - - if data["request_ip_lat"] then - cache_new_city_geocode(data) + -- If this value is marked as "pending" then we've hit an edge case where the + -- initial proxy is being logged before the api backend proxy. This shouldn't + -- happen frequently, but does sometimes crop up in testing. When this + -- happens, we'll defer logging for a second to give the backend proxy a + -- chance to finish it's logging (which will set the shared variable). + -- + -- But we want to generally avoid setting timers to log each individual + -- request, since there's a limit to how many timers we can have. So that's + -- why we only do it the handle this edge case that should be rare. + local log_timing_id = id .. "_upstream_response_time" + data["_timer_backend_response"] = ngx.shared.logs:get(log_timing_id) + if data["_timer_backend_response"] == "pending" then + ngx.timer.at(1, combine_log_data, data) + else + combine_log_data(false, data) end end -local ok, err = pcall(log_request) +local ok, err = pcall(build_log_data) if not ok then ngx.log(ngx.ERR, "failed to log request: ", err) end diff --git a/src/api-umbrella/proxy/load_backends.lua b/src/api-umbrella/proxy/load_backends.lua index e35953775..171b61bc3 100644 --- a/src/api-umbrella/proxy/load_backends.lua +++ b/src/api-umbrella/proxy/load_backends.lua @@ -28,7 +28,7 @@ local function generate_upstream_config(api) upstream = upstream .. balance .. ";\n" end - local keepalive = api["keepalive_connections"] or 10 + local keepalive = api["keepalive_connections"] or config["router"]["api_backends"]["keepalive_connections"] upstream = upstream .. "keepalive " .. keepalive .. ";\n" local servers = {} diff --git a/src/api-umbrella/proxy/middleware/rate_limit.lua b/src/api-umbrella/proxy/middleware/rate_limit.lua index 6a54973a8..6619f7047 100644 --- a/src/api-umbrella/proxy/middleware/rate_limit.lua +++ b/src/api-umbrella/proxy/middleware/rate_limit.lua @@ -201,11 +201,16 @@ return function(settings, user) end local current_time = math.floor(ngx.now() * 1000) + local test_env_skip_increment_limits = false if config["app_env"] == "test" then local fake_time = ngx.var.http_x_fake_time if fake_time then current_time = tonumber(fake_time) end + + if ngx.var.http_x_api_umbrella_test_skip_increment_limits == "true" then + test_env_skip_increment_limits = true + end end -- First check to see if the current request is over any rate limits. @@ -214,8 +219,13 @@ return function(settings, user) -- If the request isn't over any limits, then increment all the rate limit -- values (we only do this when not over limits so that over rate limit -- requests don't count against the user). - if not over_limit then + if not over_limit and not test_env_skip_increment_limits then over_limit = increment_all_limits(settings) + elseif test_env_skip_increment_limits then + -- If we're in the test environment and incrementing rate limits is + -- disabled, then add 1 back to the remaining count (since this hit hasn't + -- actually subtracted 1). + ngx.ctx.response_header_remaining = ngx.ctx.response_header_remaining + 1 end if ngx.ctx.response_header_limit then diff --git a/src/api-umbrella/proxy/middleware/rewrite_response.lua b/src/api-umbrella/proxy/middleware/rewrite_response.lua index 39d1d28d7..be05bf254 100644 --- a/src/api-umbrella/proxy/middleware/rewrite_response.lua +++ b/src/api-umbrella/proxy/middleware/rewrite_response.lua @@ -126,5 +126,17 @@ return function(settings) set_override_headers(settings) end + if config["app_env"] == "test" then + if ngx.var.http_x_api_umbrella_test_debug_workers == "true" then + ngx.header["X-Api-Umbrella-Test-Worker-Id"] = ngx.worker.id() + ngx.header["X-Api-Umbrella-Test-Worker-Count"] = ngx.worker.count() + ngx.header["X-Api-Umbrella-Test-Worker-Pid"] = ngx.worker.pid() + end + + if ngx.var.http_x_api_umbrella_test_return_request_id == "true" then + ngx.header["X-Api-Umbrella-Test-Request-Id"] = ngx.var.x_api_umbrella_request_id + end + end + rewrite_redirects() end diff --git a/src/api-umbrella/proxy/startup/seed_database.lua b/src/api-umbrella/proxy/startup/seed_database.lua index 30b6867e1..a2349426e 100644 --- a/src/api-umbrella/proxy/startup/seed_database.lua +++ b/src/api-umbrella/proxy/startup/seed_database.lua @@ -140,6 +140,7 @@ local function seed_initial_superusers() local data = { username = username, superuser = true, + registration_source = "seed", } if admin then diff --git a/src/api-umbrella/utils/mongo.lua b/src/api-umbrella/utils/mongo.lua index 04f225507..179ee4081 100644 --- a/src/api-umbrella/utils/mongo.lua +++ b/src/api-umbrella/utils/mongo.lua @@ -1,8 +1,10 @@ local cjson = require "cjson" local http = require "resty.http" +local stringx = require "pl.stringx" local types = require "pl.types" local is_empty = types.is_empty +local startswith = stringx.startswith local _M = {} @@ -68,22 +70,39 @@ local function perform_query(path, query_options, http_options) local response, err = try_query(path, http_options) - -- If we get an "EOF" error from Mora, this means our query occurred during - -- the middle of a server or replicaset change. In this case, retry the - -- request a couple more times. + -- If we certain types of errors from Mora, this means our query occurred + -- during the middle of a server or replicaset change. In this case, retry + -- the request a few more times. -- -- This should be less likely in mora since -- https://github.com/emicklei/mora/pull/29, but it's still possible for this -- to crop up if the socket gets closed sometime between the request starting - -- and the query actually executing. After more research, this seems to be + -- and the query actually executing. This can also happen in case of + -- unexpected mongod shutdowns. After more research, this seems to be -- expected mgo behavior, and it's up to the app to handle these type of -- errors. I'm not entirely sure whether we should try to address the issue -- in mora itself, but in the meantime, we'll retry here. - if err and err == "mongodb error: EOF" then - response, err = try_query(path, http_options) - if err and err == "mongodb error: EOF" then - ngx.sleep(0.5) - response, err = try_query(path, http_options) + if err then + -- Loop to retry a few times until no errors occurs or we give up, since we + -- don't want to wait forever. + local retries = 0 + while err and retries < 5 do + if err == "mongodb error: EOF" + or err == "mongodb error: node is recovering" + or err == "mongodb error: interrupted at shutdown" + or err == "mongodb error: Closed explicitly" + or startswith(err, "mongodb error: read tcp") + then + -- Retry immediately, then sleep between further retries. + retries = retries + 1 + if retries > 1 then + ngx.sleep(0.5) + end + + response, err = try_query(path, http_options) + else + break + end end end diff --git a/src/api-umbrella/web-app/.bundlerauditignore b/src/api-umbrella/web-app/.bundlerauditignore deleted file mode 100644 index f9ad0a4fa..000000000 --- a/src/api-umbrella/web-app/.bundlerauditignore +++ /dev/null @@ -1,18 +0,0 @@ -# Documenting gem security issues reported by bundler-audit that are tricky to -# upgrade, but we've manually verified we're not vulnerable to. -# -# Support for this .bundlerauditignore when using bundle-audit currently -# requires this patch: https://github.com/rubysec/bundler-audit/pull/122 - -# devise: Not relevant since we're not using Remember Me cookies. -CVE-2015-8314 - -# handlebars: We're not vulnerable since we don't have any unquoted variables -# (eg, attr={{val}} instead of attr="{{value}}"). But it would still be good to -# address, which we'll do whenever we upgrade to a newer version of Ember -# (https://github.com/NREL/api-umbrella/tree/admin-upgrade). -OSVDB-131671 - -# mail: Can't upgrade due to Rails 3.2, but our lib/mail_sanitizer.rb addresses -# the underlying issue by raising errors for problematic addresses. -OSVDB-131677 diff --git a/src/api-umbrella/web-app/.gitignore b/src/api-umbrella/web-app/.gitignore index be48804a6..880117669 100644 --- a/src/api-umbrella/web-app/.gitignore +++ b/src/api-umbrella/web-app/.gitignore @@ -23,19 +23,13 @@ # Ignore temp files /tmp -# Ignore generated sprite images -/app/assets/generated_sprites/* - # Local settings files config/settings.local.yml config/settings/*.local.yml config/environments/*.local.yml # Test reports +/spec/examples.txt /spec/reports /coverage /brakeman.html - -# Generated assets -/public/test-assets -/public/web-assets diff --git a/src/api-umbrella/web-app/.rspec b/src/api-umbrella/web-app/.rspec index 88d0c2228..83e16f804 100644 --- a/src/api-umbrella/web-app/.rspec +++ b/src/api-umbrella/web-app/.rspec @@ -1,4 +1,2 @@ --color ---backtrace ---format Fuubar ---format RspecJunitFormatter --out spec/reports/web-app.xml +--require spec_helper diff --git a/src/api-umbrella/web-app/.rubocop.yml b/src/api-umbrella/web-app/.rubocop.yml deleted file mode 100644 index 2c3425207..000000000 --- a/src/api-umbrella/web-app/.rubocop.yml +++ /dev/null @@ -1,147 +0,0 @@ -AllCops: - Exclude: - - tmp/**/* - - vendor/**/* - - config/compass.rb - - public/web-assets/**/* - - public/test-assets/**/* - RunRailsCops: true - -Lint/LiteralInInterpolation: - Enabled: false - -Lint/NumericLiterals: - Enabled: false - -Lint/UnusedBlockArgument: - Enabled: false - -Lint/UnusedMethodArgument: - Enabled: false - -Metrics/AbcSize: - Enabled: false - -Metrics/BlockNesting: - Enabled: false - -Metrics/ClassLength: - Enabled: false - -Metrics/CyclomaticComplexity: - Enabled: false - -Metrics/LineLength: - Enabled: false - -Metrics/MethodLength: - Enabled: false - -Metrics/PerceivedComplexity: - Enabled: false - -Style/AlignHash: - Enabled: false - -Style/AlignParameters: - Enabled: false - -Style/BracesAroundHashParameters: - Enabled: false - -Style/ClassAndModuleChildren: - Enabled: false - -Style/ClassCheck: - Enabled: false - -Style/CollectionMethods: - PreferredMethods: - find: 'detect' - -Style/Documentation: - Enabled: false - -Style/EachWithObject: - Enabled: false - -Style/Encoding: - Enabled: false - -Style/GuardClause: - Enabled: false - -Style/HashSyntax: - EnforcedStyle: hash_rockets - -Style/IfUnlessModifier: - Enabled: false - -Style/IndentHash: - EnforcedStyle: consistent - -Style/Lambda: - Enabled: false - -Style/NegatedIf: - Enabled: false - -Style/Next: - Enabled: false - -Style/OneLineConditional: - Enabled: false - -Style/ParenthesesAroundCondition: - Enabled: false - -Style/RaiseArgs: - Enabled: false - -Style/RedundantSelf: - Enabled: false - -Style/RegexpLiteral: - Enabled: false - -Style/SignalException: - Enabled: false - -Style/SpaceAfterControlKeyword: - Enabled: false - -Style/StringLiterals: - Enabled: false - -Style/StringLiteralsInInterpolation: - Enabled: false - -Style/SymbolProc: - Enabled: false - -Style/TrailingComma: - Enabled: false - -Style/WhileUntilModifier: - Enabled: false - -Style/WordArray: - Enabled: false - -# Not valid for Rails 3. -Rails/ActionFilter: - Enabled: false - -# Not valid for mongoid -Rails/DefaultScope: - Enabled: false - -Rails/Delegate: - Enabled: false - -Rails/HasAndBelongsToMany: - Enabled: false - -Rails/Output: - Exclude: - - db/migrate/** diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index 4c31680f0..545d77788 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -1,301 +1,136 @@ source "https://rubygems.org" -gem "rails", "~> 3.2.22.2" +gem "rails", "~> 4.2.7.1" # Rails app server -gem "puma", "~> 2.11.3" +gem "puma", "~> 3.6.0" # Error notification service (optional) -gem "rollbar", "~> 2.10.0" - -# Environment specific configuration -gem "dotenv-rails", "~> 1.0.2" +gem "rollbar", "~> 2.12.0" # Abort requests that take too long -# Don't upgrade to 0.1 yet, since it causes issues with Capybara test: -# https://github.com/heroku/rack-timeout/issues/55 -gem "rack-timeout", "~> 0.0.4" +gem "rack-timeout", "~> 0.4.2" # For proxying HTTP requests to password-protected places for admins. -gem "rack-proxy", "~> 0.5.17" +gem "rack-proxy", "~> 0.6.0" # JSON handling -gem "multi_json", "~> 1.11.0" -gem "oj", "~> 2.13.0", :platforms => [:ruby] +gem "multi_json", "~> 1.12.1" +gem "oj", "~> 2.17.4", :platforms => [:ruby] + +# Use Oj for the default Rails to_json calls. gem "oj_mimic_json", "~> 1.0.1", :platforms => [:ruby] -gem "sequel", "~> 4.31.0" +# SQL escape libraries for Kylin analytics. +gem "sequel", "~> 4.37.0" # MongoDB -gem "mongoid", "~> 3.1.6" +gem "mongoid", "~> 5.1.3" -# Created/updated userstamping -# Hold on the 0.3 release until we tackle the migration necessary for 0.4: -# https://github.com/tbpro/mongoid_userstamp/blob/master/CHANGELOG.md#040---2014-02-24 -gem "mongoid_userstamp", "~> 0.3.2" +# Soft-delete +gem "mongoid-paranoia", "~> 2.0.0" # Versioning for mongoid gem "mongoid_delorean", "~> 1.3.0" # Display deeply nested validation errors on embedded documents. -gem "mongoid-embedded-errors", "~> 2.0.1" +# +# Fork to fix Mongoid 4+ compatibility: +# https://github.com/glooko/mongoid-embedded-errors/pull/6 +gem "mongoid-embedded-errors", "~> 2.0.1", :git => "https://github.com/calfzhou/mongoid-embedded-errors.git" # Data migrations -gem "mongoid_rails_migrations", "~> 1.0.1" +gem "mongoid_rails_migrations", "~> 1.1.0" # Rails cache store using mongo. -gem "mongoid-store", "~> 0.4.4" - -# Generate UUIDs -gem "uuidtools", "~> 2.1.4" +# +# Use master from git for Rails 4 compatibility. +gem "mongoid-store", :git => "https://github.com/ahoward/mongoid-store.git" # Database seeding +# # This branch adds mongoid compatibility: # https://github.com/mbleigh/seed-fu/pull/80 gem "seed-fu", :git => "https://github.com/GUI/seed-fu.git", :branch => "mongoid" # Elasticsearch -gem "elasticsearch", "~> 1.0.14" +gem "elasticsearch", "~> 2.0.0" # OmniAuth-based authentication -gem "devise", "~> 3.4.0" -gem "omniauth", "~> 1.2.1" -gem "omniauth-cas", "~> 1.1.0", :git => "https://github.com/GUI/omniauth-cas.git", :branch => "rexml" -gem "omniauth-facebook", "~> 2.0.0" -# Patched omniauth-github for e-mail verification: -# https://github.com/intridea/omniauth-github/pull/41 -gem "omniauth-github", :git => "https://github.com/riking/omniauth-github.git", :branch => "all_emails" -gem "omniauth-google-oauth2", "~> 0.2.2" -# Latest from git to loosen net-ldap dependency -# (https://github.com/intridea/omniauth-ldap/pull/46) so we can bump net-ldap -# for OSVDB-106108 -gem "omniauth-ldap", "~> 1.0.4", :git => "https://github.com/intridea/omniauth-ldap.git" -gem "omniauth-myusa", :git => "https://github.com/GSA-OCSIT/omniauth-myusa.git" -gem "omniauth-persona", "~> 0.0.1" -gem "omniauth-twitter", "~> 1.0.1" +gem "devise", "~> 4.2.0" +gem "omniauth", "~> 1.3.1" +gem "omniauth-cas", "~> 1.1.0", :git => "https://github.com/GUI/omniauth-cas.git", :branch => "rexml", :require => false +gem "omniauth-facebook", "~> 4.0.0", :require => false +# Use master for e-mail verification: +# https://github.com/intridea/omniauth-github/pull/48 +gem "omniauth-github", :git => "https://github.com/intridea/omniauth-github.git", :require => false +gem "omniauth-google-oauth2", "~> 0.4.1", :require => false +gem "omniauth-ldap", "~> 1.0.5", :require => false +gem "omniauth-persona", "~> 0.0.1", :require => false +gem "omniauth-twitter", "~> 1.2.1", :require => false # Authorization gem "pundit", "~> 1.1.0" -# Pagination -gem "kaminari", "~> 0.16.1" -gem "kaminari-bootstrap", "~> 0.1.3" # Hold at bootstrap 2 version - -# Navigation links -gem "tabs_on_rails", "~> 2.2.0" - -# Unobtrusive javascript for Rails helpers (things like delete links). -gem "jquery-rails", "~> 3.1.4" +# Generate non-digest assets for i18n content that the admin-ui component can +# link to (without knowing the cache busted URLs). +gem "non-stupid-digest-assets", "~> 1.0.8" # Views/templates for APIs -gem "rabl", "~> 0.11.5" -gem "jbuilder", "~> 2.2.2" +gem "rabl", "~> 0.13.0" +gem "jbuilder", "~> 2.6.0" gem "csv_builder", "~> 2.1.1" # Country and state name lookups -gem "countries", "~> 0.11.3" - -# HTML diffs -gem "diffy", "~> 3.0.7" - -# Use a newer version of Psych for YAML. The newer gem version does a better -# job of making multi-line strings and strings with colons in them more human -# readable. -gem "psych", "~> 2.0.13", :platforms => [:ruby] +gem "countries", "~> 1.2.5" # For user-inputted YAML. gem "safe_yaml", "~> 1.0.4", :require => "safe_yaml/load" # Delayed jobs and background tasks -gem "delayed_job_mongoid", "~> 2.1.0" -gem "daemons", "~> 1.2.2" +gem "delayed_job_mongoid", "~> 2.2.0" +gem "daemons", "~> 1.2.4" # HTML email styling # nokogiri is a soft-dependency for premailer-rails so we need to explicitly # include it here. -gem "premailer-rails", "~> 1.8.0" +gem "premailer-rails", "~> 1.9.4" gem "nokogiri", "~> 1.6.8" -gem "mail", "~> 2.5.4" - # Localization in the admin based on the Accept-Language header -gem "http_accept_language", "~> 2.0.5" +gem "http_accept_language", "~> 2.1.0" # Markdown -gem "kramdown", "~> 1.6.0" +gem "kramdown", "~> 1.12.0" # Lucene query parsing for translating into SQL. gem "lucene_query_parser", :git => "https://github.com/zerowidth/lucene_query_parser.git" -# Include test-unit so rspec 2 can run under Ruby 2.2: -# https://github.com/rspec/rspec-rails/issues/1273 -# -# Include in production bundle (not just development/test), since -# active_model_serializers (which we don't use, but ember-rails forces as a -# dependency) tries to monkey-patch ActionController::TestCase at load-time -# (https://github.com/rails-api/active_model_serializers/blob/v0.9.3/lib/active_model_serializers.rb#L15). -# This fails under Ruby 2.2 if we don't also have this test-unit gem in -# production. Sigh... -gem "test-unit", :require => false - -group :production, :staging do - # Log to stdout instead of file - gem "rails_stdout_logging", "~> 0.0.3" -end +# Thread-safe per-request variables. +gem "request_store", "~> 1.3.1" -# Gems used only for assets and not required -# in production environments by default. -group :assets do - gem 'sass-rails', '~> 3.2.6' - - # Hold at sass 3.2, since newer versions lead to weird sprockets errors. - # Apparently fixed in newer versions of sprockets, but not the version Rails - # 3.2 uses: - # https://github.com/sass/sass/issues/1144 - gem "sass", "~> 3.2.19" - - # A Sass version of Twitter Bootstrap. This it the basis for our styles and - # JavaScript components. - gem "bootstrap-sass", "~> 2.3.2.2" - - # Sass utilities and automatic image spirtes - gem "compass-rails", "~> 1.1.7" - - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - gem "therubyracer", "~> 0.12.2", :platforms => :ruby - # For JRuby, use the Node.js execjs runtime - We'll assume it's on the - # servers so it gets picked up by execjs. It's faster than therubyrhino. - - # JavaScript compression - gem "uglifier", "~> 2.7.1" - - # Smarter handling of compiled CSS with relative paths (like Jammit) - gem "sprockets-urlrewriter", "~> 0.1.2" - - # Faster asset precompilation and caching. - # This specific version contains the CLEAN_EXPIRED_ASSETS option to speed up - # deployments by combining two tasks into one (particularly under JRuby). - gem "turbo-sprockets-rails3", "0.3.13" - - # Ember.js - gem "ember-rails", "~> 0.15.0" - gem "ember-source", "~> 1.7.1" - - # We're not using this ourselves (ember-rails is pulling it in), but if it gets - # updated to 0.9.1 it seems to break various things in our responses. I think - # the 0.9.1 release might be slightly busted based on these reports: - # https://github.com/rails-api/active_model_serializers/issues/747 - # https://github.com/rails-api/active_model_serializers/issues/746 - # Lock to 0.9.0 for now. - gem "active_model_serializers", "0.9.0" - - source "https://rails-assets.org" do - # Client-side translations - gem "rails-assets-polyglot", "~> 0.4.1" - - # Smooth scrolling to content - gem "rails-assets-jquery.scrollTo", "~> 1.4.14" - - # Icons - gem "rails-assets-font-awesome", "~> 4.2.0" - - # Code editor (for syntax highlighting inside textareas) - gem "rails-assets-ace-builds", "~> 1.1.7" - - # Visual text diffs - gem "rails-assets-jsdiff", "~> 1.0.8" - - # jQuery ajax calls wrapped in Ember promises - gem "rails-assets-ic-ajax", "~> 2.0.1" - gem "rails-assets-ember", "~> 1.7.1" - - # For Markdown parsing - gem "rails-assets-marked", "~> 0.3.2" - - gem "rails-assets-bootbox", "~> 3.3.0" - gem "rails-assets-bootstrap-daterangepicker", "~> 1.3.12" - gem "rails-assets-datatables", "~> 1.10.2" - gem "rails-assets-html5shiv", "~> 3.7.0" - gem "rails-assets-inflection", "~> 1.4.0" - gem "rails-assets-jquery", "~> 1.11.2" - gem "rails-assets-jquery-bbq-deparam", "~> 1.2.1" - gem "rails-assets-jstz-detect", "~> 1.0.5" - gem "rails-assets-lodash", "~> 2.4.1" - gem "rails-assets-moment", "~> 2.8.2" - gem "rails-assets-numeral", "~> 1.5.3" - gem "rails-assets-pnotify", "~> 2.0.1" - gem "rails-assets-qtip2", "~> 2.2.0" - gem "rails-assets-selectize", "~> 0.11.2" - gem "rails-assets-spinjs", "~> 2.0.0" - end -end +# Share i18n data with the admin-ui client-side app. +gem "i18n-js", ">= 3.0.0.rc13" + +# Log to stdout instead of file +gem "rails_stdout_logging", "~> 0.0.5", :require => false # Bundle gems for the local environment. Make sure to # put test-only gems in this group so their generators # and rake tasks are available in development mode: group :development, :test do - gem "rspec-rails", "~> 2.99.0" - gem "factory_girl_rails", "~> 4.5.0" - gem "rspec-html-matchers", "~> 0.5.0" - - # Rspec formatter - Prints overall progress and error details as they happen. - gem "fuubar", "~> 1.3.3" - - # Ruby lint/style checker - gem "rubocop", "~> 0.27.1", :require => false - - # Real browser testing - gem "capybara", "~> 2.5.0" - - # Take screenshots and save the HTML content whenever capybara errors occur - # for better debugging. - gem "capybara-screenshot", "~> 1.0.11" - - # Headless webkit for capybara - gem "poltergeist", "~> 1.6.0" - - # Clean the database between tests - gem "database_cleaner", "~> 1.4.1" - - # JavaScript lint/style checker - gem "jshintrb", "~> 0.3.0" - - source "https://rails-assets.org" do - # For testing drag and drop in capybara. - gem "rails-assets-jquery-simulate-ext", "~> 1.3.0" - end - - # For creating test data in elasticsearch. - gem "elasticsearch-persistence", "~> 0.1.7" - - # For parsing URLs in tests. - gem "addressable", "~> 2.3.8" - - # For caching external web requests - gem "vcr", "~> 2.9.3", :require => false - gem "webmock", "~> 1.21.0", :require => false - - # Generate output for CI. - gem "rspec_junit_formatter", "~> 0.2.3" - - # For running the api-umbrella process as a background process during tests. - gem "childprocess", "~> 0.5.9" - # Check for security issues on gem dependencies. - # - # Use this fork for .bundlerauditignore support: - # https://github.com/rubysec/bundler-audit/pull/122 - gem "bundler-audit", :git => "https://github.com/rickypai/bundler-audit.git", :branch => "rpai/ignorefile", :require => false + gem "bundler-audit", :require => false # Check for application security issues. gem "brakeman", :require => false # Debug inspecting/printing. - gem "awesome_print", "~> 1.6.1" + gem "awesome_print", "~> 1.7.0" end group :development do # Deployment - gem "capistrano", "~> 3.3.5" - gem "capistrano-rails", "~> 1.1.2" + gem "capistrano", "~> 3.6.1" + gem "capistrano-rails", "~> 1.1.7" end diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index 417675c66..2a8fab012 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -1,55 +1,42 @@ -GIT - remote: https://github.com/GSA-OCSIT/omniauth-myusa.git - revision: d918fa88b6b8e9d835a38afea9e9efaf015a74e4 - specs: - omniauth-myusa (0.0.4) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.0) - GIT remote: https://github.com/GUI/omniauth-cas.git - revision: ad88e1ef548430cf68c7a5dd9a9725e304b19f56 + revision: 45b3634c7943fa0e0eec08360e96486576be1100 branch: rexml specs: omniauth-cas (1.1.0) addressable (~> 2.3) - omniauth (~> 1.2.0) + omniauth (~> 1.2) GIT remote: https://github.com/GUI/seed-fu.git - revision: 616758da2962b5870869a35a374ee6c890a7b33b + revision: 7b265f853514923bf1e2ce9908d6134d69b3ad5c branch: mongoid specs: - seed-fu (2.3.4) - activesupport (>= 3.1, <= 4.2) + seed-fu (2.3.6) + activesupport (>= 3.1) GIT - remote: https://github.com/intridea/omniauth-ldap.git - revision: 9d36cdb9f3d4da040ab6f7aff54450392b78f5eb + remote: https://github.com/ahoward/mongoid-store.git + revision: 7b7e34e263a1e25fc0149861e1560f7f483c16ac specs: - omniauth-ldap (1.0.4) - net-ldap (~> 0.3) - omniauth (~> 1.0) - pyu-ruby-sasl (~> 0.0.3.1) - rubyntlm (~> 0.3) + mongoid-store (0.5.0) + activesupport (>= 3) + mongoid (>= 3) GIT - remote: https://github.com/rickypai/bundler-audit.git - revision: 1781f4944f2e2f11358a05240091cb29ed70f906 - branch: rpai/ignorefile + remote: https://github.com/calfzhou/mongoid-embedded-errors.git + revision: 4add4cd70154c34f79b5db8781ef0b13da23cec9 specs: - bundler-audit (0.4.0) - bundler (~> 1.2) - thor (~> 0.18) + mongoid-embedded-errors (2.0.1) + mongoid (>= 3.0.0) GIT - remote: https://github.com/riking/omniauth-github.git - revision: 8d1f8b665501dd27bae941ea8e128597b7e8edea - branch: all_emails + remote: https://github.com/intridea/omniauth-github.git + revision: 45f2fc73d6d06f30863adac0e6aa112bcaaadf67 specs: omniauth-github (1.1.2) omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) + omniauth-oauth2 (>= 1.1.1, < 2.0) GIT remote: https://github.com/zerowidth/lucene_query_parser.git @@ -61,596 +48,322 @@ GIT GEM remote: https://rubygems.org/ - remote: https://rails-assets.org/ specs: - actionmailer (3.2.22.5) - actionpack (= 3.2.22.5) - mail (~> 2.5.4) - actionpack (3.2.22.5) - activemodel (= 3.2.22.5) - activesupport (= 3.2.22.5) - builder (~> 3.0.0) + actionmailer (4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.7.1) + actionview (= 4.2.7.1) + activesupport (= 4.2.7.1) + rack (~> 1.6) + rack-test (~> 0.6.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.7.1) + activesupport (= 4.2.7.1) + builder (~> 3.1) erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.5) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.2.1) - active_model_serializers (0.9.0) - activemodel (>= 3.2) - activemodel (3.2.22.5) - activesupport (= 3.2.22.5) - builder (~> 3.0.0) - activerecord (3.2.22.5) - activemodel (= 3.2.22.5) - activesupport (= 3.2.22.5) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activeresource (3.2.22.5) - activemodel (= 3.2.22.5) - activesupport (= 3.2.22.5) - activesupport (3.2.22.5) - i18n (~> 0.6, >= 0.6.4) - multi_json (~> 1.0) - addressable (2.3.8) - arel (3.0.3) - ast (2.0.0) - astrolabe (1.3.0) - parser (>= 2.2.0.pre.3, < 3.0) - awesome_print (1.6.1) - axiom-types (0.1.1) - descendants_tracker (~> 0.0.4) - ice_nine (~> 0.11.0) - thread_safe (~> 0.3, >= 0.3.1) - barber (0.9.0) - ember-source (>= 1.0, < 2) - execjs (>= 1.2, < 3) - bcrypt (3.1.10) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (4.2.7.1) + activesupport (= 4.2.7.1) + globalid (>= 0.3.0) + activemodel (4.2.7.1) + activesupport (= 4.2.7.1) + builder (~> 3.1) + activerecord (4.2.7.1) + activemodel (= 4.2.7.1) + activesupport (= 4.2.7.1) + arel (~> 6.0) + activesupport (4.2.7.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.4.0) + airbrussh (1.1.1) + sshkit (>= 1.6.1, != 1.7.0) + arel (6.0.3) + awesome_print (1.7.0) + bcrypt (3.1.11) blankslate (3.1.3) - bootstrap-sass (2.3.2.2) - sass (~> 3.2) - brakeman (3.2.1) - erubis (~> 2.6) - haml (>= 3.0, < 5.0) - highline (>= 1.6.20, < 2.0) - ruby2ruby (~> 2.3.0) - ruby_parser (~> 3.8.1) - safe_yaml (>= 1.0) - sass (~> 3.0) - slim (>= 1.3.6, < 4.0) - terminal-table (~> 1.4) - builder (3.0.4) - capistrano (3.3.5) - capistrano-stats (~> 1.1.0) + brakeman (3.4.0) + bson (4.1.1) + builder (3.2.2) + bundler-audit (0.5.0) + bundler (~> 1.2) + thor (~> 0.18) + capistrano (3.6.1) + airbrussh (>= 1.0.0) + capistrano-harrow i18n rake (>= 10.0.0) - sshkit (~> 1.3) + sshkit (>= 1.9.0) capistrano-bundler (1.1.4) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-rails (1.1.2) + capistrano-harrow (0.5.3) + capistrano-rails (1.1.8) capistrano (~> 3.1) capistrano-bundler (~> 1.1) - capistrano-stats (1.1.1) - capybara (2.5.0) - mime-types (>= 1.16) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) - capybara-screenshot (1.0.11) - capybara (>= 1.0, < 3) - launchy - childprocess (0.5.9) - ffi (~> 1.0, >= 1.0.11) - chunky_png (1.3.4) - cliver (0.3.2) - coercible (1.0.0) - descendants_tracker (~> 0.0.1) - colorize (0.7.5) - compass (0.12.7) - chunky_png (~> 1.2) - fssm (>= 0.2.7) - sass (~> 3.2.19) - compass-rails (1.1.7) - compass (>= 0.12.2) - sprockets (<= 2.11.0) - countries (0.11.3) + concurrent-ruby (1.0.2) + countries (1.2.5) currencies (~> 0.4.2) - i18n_data (~> 0.6.0) - crack (0.4.2) - safe_yaml (~> 1.0.0) - css_parser (1.3.6) + i18n_data (~> 0.7.0) + css_parser (1.4.5) addressable csv_builder (2.1.1) actionpack (>= 3.0.0) currencies (0.4.2) - daemons (1.2.2) - database_cleaner (1.4.1) - delayed_job (4.0.6) - activesupport (>= 3.0, < 5.0) - delayed_job_mongoid (2.1.0) + daemons (1.2.4) + delayed_job (4.1.2) + activesupport (>= 3.0, < 5.1) + delayed_job_mongoid (2.2.0) delayed_job (>= 3.0, < 5) - mongoid (>= 3.0, < 5) - descendants_tracker (0.0.4) - thread_safe (~> 0.3, >= 0.3.1) - devise (3.4.1) + mongoid (>= 3.0, < 6) + mongoid-compatibility + devise (4.2.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0, < 5.1) responders - thread_safe (~> 0.1) warden (~> 1.2.3) - diff-lcs (1.2.5) - diffy (3.0.7) - dotenv (1.0.2) - dotenv-rails (1.0.2) - dotenv (= 1.0.2) - elasticsearch (1.0.14) - elasticsearch-api (= 1.0.14) - elasticsearch-transport (= 1.0.14) - elasticsearch-api (1.0.14) + elasticsearch (2.0.0) + elasticsearch-api (= 2.0.0) + elasticsearch-transport (= 2.0.0) + elasticsearch-api (2.0.0) multi_json - elasticsearch-model (0.1.7) - activesupport (> 3) - elasticsearch (> 0.4) - hashie - elasticsearch-persistence (0.1.7) - activemodel (> 3) - activesupport (> 3) - elasticsearch (> 0.4) - elasticsearch-model (>= 0.1) - hashie - virtus - elasticsearch-transport (1.0.14) + elasticsearch-transport (2.0.0) faraday multi_json - ember-data-source (1.0.0.beta.14) - ember-source - ember-rails (0.15.1) - active_model_serializers - barber (>= 0.4.1) - ember-data-source (>= 1.0.0.beta.5) - ember-source (>= 1.1.0) - execjs (>= 1.2) - handlebars-source (> 1.0.0) - jquery-rails (>= 1.0.17) - railties (>= 3.1) - ember-source (1.7.1) - handlebars-source (~> 1.0) - equalizer (0.0.11) erubis (2.7.0) - execjs (2.6.0) - factory_girl (4.5.0) - activesupport (>= 3.0.0) - factory_girl_rails (4.5.0) - factory_girl (~> 4.5.0) - railties (>= 3.0.0) faraday (0.9.2) multipart-post (>= 1.2, < 3) - ffi (1.9.10) - fssm (0.2.10) - fuubar (1.3.3) - rspec (>= 2.14.0, < 3.1.0) - ruby-progressbar (~> 1.4) - haml (4.0.7) - tilt - handlebars-source (1.3.0) - hashie (3.4.3) - highline (1.7.8) - hike (1.2.3) - htmlentities (4.3.3) - http_accept_language (2.0.5) + globalid (0.3.7) + activesupport (>= 4.1.0) + hashie (3.4.6) + htmlentities (4.3.4) + http_accept_language (2.1.0) i18n (0.7.0) - i18n_data (0.6.3) - ice_nine (0.11.1) - jbuilder (2.2.13) - activesupport (>= 3.0.0, < 5) + i18n-js (3.0.0.rc14) + i18n (~> 0.6, >= 0.6.6) + i18n_data (0.7.0) + jbuilder (2.6.0) + activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - journey (1.0.4) - jquery-rails (3.1.4) - railties (>= 3.0, < 5.0) - thor (>= 0.14, < 2.0) - jshintrb (0.3.0) - execjs - multi_json (>= 1.3) - rake json (1.8.3) - jwt (1.5.1) - kaminari (0.16.3) - actionpack (>= 3.0.0) - activesupport (>= 3.0.0) - kaminari-bootstrap (0.1.3) - kaminari (>= 0.13.0) - rails - kramdown (1.6.0) - launchy (2.4.3) - addressable (~> 2.3) - libv8 (3.16.14.13) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) - mime-types (1.25.1) + jwt (1.5.6) + kramdown (1.12.0) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) mini_portile2 (2.1.0) - mongoid (3.1.7) - activemodel (~> 3.2) - moped (~> 1.4) - origin (~> 1.0) - tzinfo (~> 0.3.29) - mongoid-embedded-errors (2.0.1) - mongoid (>= 3.0.0) - mongoid-store (0.4.4) - activesupport (~> 3.2) - mongoid (~> 3.0) + minitest (5.9.1) + mongo (2.3.0) + bson (~> 4.1) + mongoid (5.1.4) + activemodel (~> 4.0) + mongo (~> 2.1) + origin (~> 2.2) + tzinfo (>= 0.3.37) + mongoid-compatibility (0.4.0) + activesupport + mongoid (>= 2.0) + mongoid-paranoia (2.0.0) + activesupport (~> 4.0) + mongoid (>= 4.0.0, <= 6.0.0) mongoid_delorean (1.3.0) mongoid - mongoid_rails_migrations (1.0.1) - activesupport (>= 3.2.0) + mongoid_rails_migrations (1.1.0) + activesupport (>= 4.2.0) bundler (>= 1.0.0) - rails (>= 3.2.0) - railties (>= 3.2.0) - mongoid_userstamp (0.3.2) - mongoid (>= 3.0.4) - moped (1.5.3) - multi_json (1.11.3) + mongoid (>= 4.0.0) + rails (>= 4.2.0) + railties (>= 4.2.0) + multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) - net-ldap (0.13.0) + net-ldap (0.15.0) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (2.9.2) + net-ssh (3.2.0) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) - oauth (0.4.7) - oauth2 (1.0.0) + non-stupid-digest-assets (1.0.8) + sprockets (>= 2.0) + oauth (0.5.1) + oauth2 (1.2.0) faraday (>= 0.8, < 0.10) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) - rack (~> 1.2) - oj (2.13.0) + rack (>= 1.2, < 3) + oj (2.17.4) oj_mimic_json (1.0.1) - omniauth (1.2.2) + omniauth (1.3.1) hashie (>= 1.2, < 4) - rack (~> 1.0) - omniauth-facebook (2.0.1) + rack (>= 1.0, < 3) + omniauth-facebook (4.0.0) omniauth-oauth2 (~> 1.2) - omniauth-google-oauth2 (0.2.6) - omniauth (> 1.0) - omniauth-oauth2 (~> 1.1) - omniauth-oauth (1.0.1) + omniauth-google-oauth2 (0.4.1) + jwt (~> 1.5.2) + multi_json (~> 1.3) + omniauth (>= 1.1.1) + omniauth-oauth2 (>= 1.3.1) + omniauth-ldap (1.0.5) + net-ldap (~> 0.12) + omniauth (~> 1.0) + pyu-ruby-sasl (~> 0.0.3.2) + rubyntlm (~> 0.3.4) + omniauth-oauth (1.1.0) oauth omniauth (~> 1.0) - omniauth-oauth2 (1.3.1) + omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) omniauth-persona (0.0.1) faraday multi_json omniauth (~> 1.0) - omniauth-twitter (1.0.1) - multi_json (~> 1.3) - omniauth-oauth (~> 1.0) - origin (1.1.0) + omniauth-twitter (1.2.1) + json (~> 1.3) + omniauth-oauth (~> 1.1) + origin (2.2.0) orm_adapter (0.5.0) - parser (2.2.0.3) - ast (>= 1.1, < 3.0) parslet (1.7.1) blankslate (>= 2.0, <= 4.0) pkg-config (1.1.7) - poltergeist (1.6.0) - capybara (~> 2.1) - cliver (~> 0.3.1) - multi_json (~> 1.0) - websocket-driver (>= 0.2.0) - polyglot (0.3.5) - power_assert (0.2.4) - powerpack (0.0.9) - premailer (1.8.4) - css_parser (>= 1.3.6) + premailer (1.8.7) + css_parser (>= 1.4.5) htmlentities (>= 4.0.0) - premailer-rails (1.8.0) - actionmailer (>= 3, < 5) + premailer-rails (1.9.4) + actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) - psych (2.0.13) - puma (2.11.3) - rack (>= 1.1, < 2.0) + puma (3.6.0) pundit (1.1.0) activesupport (>= 3.0.0) pyu-ruby-sasl (0.0.3.3) - rabl (0.11.6) + rabl (0.13.0) activesupport (>= 2.3.14) - rack (1.4.7) - rack-cache (1.6.1) - rack (>= 0.4) - rack-proxy (0.5.17) - rack - rack-ssl (1.3.4) + rack (1.6.4) + rack-proxy (0.6.0) rack rack-test (0.6.3) rack (>= 1.0) - rack-timeout (0.0.4) - rails (3.2.22.5) - actionmailer (= 3.2.22.5) - actionpack (= 3.2.22.5) - activerecord (= 3.2.22.5) - activeresource (= 3.2.22.5) - activesupport (= 3.2.22.5) - bundler (~> 1.0) - railties (= 3.2.22.5) - rails-assets-ace-builds (1.1.9) - rails-assets-bootbox (3.3.0) - rails-assets-bootstrap-daterangepicker (1.3.17) - rails-assets-jquery (>= 1.10) - rails-assets-moment (~> 2.8.0) - rails-assets-datatables (1.10.6) - rails-assets-jquery (>= 1.7.0) - rails-assets-ember (1.7.1) - rails-assets-handlebars (>= 1.0.0, < 2.0.0) - rails-assets-jquery (>= 1.7.0, < 2.2.0) - rails-assets-eventEmitter (4.2.11) - rails-assets-eventie (1.0.6) - rails-assets-font-awesome (4.2.0) - rails-assets-handlebars (1.3.0) - rails-assets-html5shiv (3.7.2) - rails-assets-ic-ajax (2.0.2) - rails-assets-ember (>= 1.2, < 2) - rails-assets-imagesloaded (3.1.8) - rails-assets-eventEmitter (~> 4.0) - rails-assets-eventie (>= 1.0.4, < 2) - rails-assets-inflection (1.4.2) - rails-assets-jquery (1.11.2) - rails-assets-jquery-bbq-deparam (1.2.1) - rails-assets-jquery-simulate-ext (1.3.0) - rails-assets-jquery (>= 1.7.0) - rails-assets-jquery.scrollTo (1.4.14) - rails-assets-jquery (>= 1.4) - rails-assets-jsdiff (1.0.8) - rails-assets-jstz-detect (1.0.5) - rails-assets-lodash (2.4.1) - rails-assets-marked (0.3.3) - rails-assets-microplugin (0.0.3) - rails-assets-moment (2.8.4) - rails-assets-numeral (1.5.3) - rails-assets-pnotify (2.0.1) - rails-assets-polyglot (0.4.2) - rails-assets-qtip2 (2.2.1) - rails-assets-imagesloaded (>= 3.0.0) - rails-assets-jquery (>= 1.6.0) - rails-assets-selectize (0.11.2) - rails-assets-jquery (>= 1.7.0) - rails-assets-microplugin (~> 0.0.0) - rails-assets-sifter (~> 0.3.0) - rails-assets-sifter (0.3.4) - rails-assets-spinjs (2.0.2) - rails_stdout_logging (0.0.3) - railties (3.2.22.5) - actionpack (= 3.2.22.5) - activesupport (= 3.2.22.5) - rack-ssl (~> 1.3.2) + rack-timeout (0.4.2) + rails (4.2.7.1) + actionmailer (= 4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + activemodel (= 4.2.7.1) + activerecord (= 4.2.7.1) + activesupport (= 4.2.7.1) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.7.1) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + rails_stdout_logging (0.0.5) + railties (4.2.7.1) + actionpack (= 4.2.7.1) + activesupport (= 4.2.7.1) rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rainbow (2.0.0) + thor (>= 0.18.1, < 2.0) + rainbow (2.1.0) rake (11.3.0) - rdoc (3.12.2) - json (~> 1.4) - ref (2.0.0) - responders (1.1.2) - railties (>= 3.2, < 4.2) - rollbar (2.10.0) + request_store (1.3.1) + responders (2.3.0) + railties (>= 4.2.0, < 5.1) + rollbar (2.12.0) multi_json - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec-collection_matchers (1.1.2) - rspec-expectations (>= 2.99.0.beta1) - rspec-core (2.99.2) - rspec-expectations (2.99.2) - diff-lcs (>= 1.1.3, < 2.0) - rspec-html-matchers (0.5.0) - nokogiri (~> 1) - rspec (~> 2, >= 2.11.0) - rspec-mocks (2.99.3) - rspec-rails (2.99.0) - actionpack (>= 3.0) - activemodel (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-collection_matchers - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec_junit_formatter (0.2.3) - builder (< 4) - rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.27.1) - astrolabe (~> 1.3) - parser (>= 2.2.0.pre.7, < 3.0) - powerpack (~> 0.0.6) - rainbow (>= 1.99.1, < 3.0) - ruby-progressbar (~> 1.4) - ruby-progressbar (1.7.5) - ruby2ruby (2.3.0) - ruby_parser (~> 3.1) - sexp_processor (~> 4.0) - ruby_parser (3.8.1) - sexp_processor (~> 4.1) - rubyntlm (0.5.2) + rubyntlm (0.3.4) safe_yaml (1.0.4) - sass (3.2.19) - sass-rails (3.2.6) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) - sequel (4.31.0) - sexp_processor (4.7.0) - slim (3.0.6) - temple (~> 0.7.3) - tilt (>= 1.3.3, < 2.1) - sprockets (2.2.3) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-urlrewriter (0.1.2) - sprockets - sshkit (1.7.1) - colorize (>= 0.7.0) + sequel (4.37.0) + sprockets (3.7.0) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sshkit (1.11.3) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - tabs_on_rails (2.2.0) - temple (0.7.6) - terminal-table (1.5.2) - test-unit (3.1.3) - power_assert - therubyracer (0.12.2) - libv8 (~> 3.16.14.0) - ref thor (0.19.1) thread_safe (0.3.5) - tilt (1.4.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) - turbo-sprockets-rails3 (0.3.13) - railties (> 3.2.8, < 4.0.0) - sprockets (>= 2.2.0) - tzinfo (0.3.52) - uglifier (2.7.2) - execjs (>= 0.3.0) - json (>= 1.8.0) - uuidtools (2.1.5) - vcr (2.9.3) - virtus (1.0.5) - axiom-types (~> 0.1) - coercible (~> 1.0) - descendants_tracker (~> 0.0, >= 0.0.3) - equalizer (~> 0.0, >= 0.0.9) - warden (1.2.3) + tzinfo (1.2.2) + thread_safe (~> 0.1) + warden (1.2.6) rack (>= 1.0) - webmock (1.21.0) - addressable (>= 2.3.6) - crack (>= 0.3.2) - websocket-driver (0.5.4) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) - xpath (2.0.0) - nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES - active_model_serializers (= 0.9.0) - addressable (~> 2.3.8) - awesome_print (~> 1.6.1) - bootstrap-sass (~> 2.3.2.2) + awesome_print (~> 1.7.0) brakeman - bundler-audit! - capistrano (~> 3.3.5) - capistrano-rails (~> 1.1.2) - capybara (~> 2.5.0) - capybara-screenshot (~> 1.0.11) - childprocess (~> 0.5.9) - compass-rails (~> 1.1.7) - countries (~> 0.11.3) + bundler-audit + capistrano (~> 3.6.1) + capistrano-rails (~> 1.1.7) + countries (~> 1.2.5) csv_builder (~> 2.1.1) - daemons (~> 1.2.2) - database_cleaner (~> 1.4.1) - delayed_job_mongoid (~> 2.1.0) - devise (~> 3.4.0) - diffy (~> 3.0.7) - dotenv-rails (~> 1.0.2) - elasticsearch (~> 1.0.14) - elasticsearch-persistence (~> 0.1.7) - ember-rails (~> 0.15.0) - ember-source (~> 1.7.1) - factory_girl_rails (~> 4.5.0) - fuubar (~> 1.3.3) - http_accept_language (~> 2.0.5) - jbuilder (~> 2.2.2) - jquery-rails (~> 3.1.4) - jshintrb (~> 0.3.0) - kaminari (~> 0.16.1) - kaminari-bootstrap (~> 0.1.3) - kramdown (~> 1.6.0) + daemons (~> 1.2.4) + delayed_job_mongoid (~> 2.2.0) + devise (~> 4.2.0) + elasticsearch (~> 2.0.0) + http_accept_language (~> 2.1.0) + i18n-js (>= 3.0.0.rc13) + jbuilder (~> 2.6.0) + kramdown (~> 1.12.0) lucene_query_parser! - mail (~> 2.5.4) - mongoid (~> 3.1.6) - mongoid-embedded-errors (~> 2.0.1) - mongoid-store (~> 0.4.4) + mongoid (~> 5.1.3) + mongoid-embedded-errors (~> 2.0.1)! + mongoid-paranoia (~> 2.0.0) + mongoid-store! mongoid_delorean (~> 1.3.0) - mongoid_rails_migrations (~> 1.0.1) - mongoid_userstamp (~> 0.3.2) - multi_json (~> 1.11.0) + mongoid_rails_migrations (~> 1.1.0) + multi_json (~> 1.12.1) nokogiri (~> 1.6.8) - oj (~> 2.13.0) + non-stupid-digest-assets (~> 1.0.8) + oj (~> 2.17.4) oj_mimic_json (~> 1.0.1) - omniauth (~> 1.2.1) + omniauth (~> 1.3.1) omniauth-cas (~> 1.1.0)! - omniauth-facebook (~> 2.0.0) + omniauth-facebook (~> 4.0.0) omniauth-github! - omniauth-google-oauth2 (~> 0.2.2) - omniauth-ldap (~> 1.0.4)! - omniauth-myusa! + omniauth-google-oauth2 (~> 0.4.1) + omniauth-ldap (~> 1.0.5) omniauth-persona (~> 0.0.1) - omniauth-twitter (~> 1.0.1) - poltergeist (~> 1.6.0) - premailer-rails (~> 1.8.0) - psych (~> 2.0.13) - puma (~> 2.11.3) + omniauth-twitter (~> 1.2.1) + premailer-rails (~> 1.9.4) + puma (~> 3.6.0) pundit (~> 1.1.0) - rabl (~> 0.11.5) - rack-proxy (~> 0.5.17) - rack-timeout (~> 0.0.4) - rails (~> 3.2.22.2) - rails-assets-ace-builds (~> 1.1.7)! - rails-assets-bootbox (~> 3.3.0)! - rails-assets-bootstrap-daterangepicker (~> 1.3.12)! - rails-assets-datatables (~> 1.10.2)! - rails-assets-ember (~> 1.7.1)! - rails-assets-font-awesome (~> 4.2.0)! - rails-assets-html5shiv (~> 3.7.0)! - rails-assets-ic-ajax (~> 2.0.1)! - rails-assets-inflection (~> 1.4.0)! - rails-assets-jquery (~> 1.11.2)! - rails-assets-jquery-bbq-deparam (~> 1.2.1)! - rails-assets-jquery-simulate-ext (~> 1.3.0)! - rails-assets-jquery.scrollTo (~> 1.4.14)! - rails-assets-jsdiff (~> 1.0.8)! - rails-assets-jstz-detect (~> 1.0.5)! - rails-assets-lodash (~> 2.4.1)! - rails-assets-marked (~> 0.3.2)! - rails-assets-moment (~> 2.8.2)! - rails-assets-numeral (~> 1.5.3)! - rails-assets-pnotify (~> 2.0.1)! - rails-assets-polyglot (~> 0.4.1)! - rails-assets-qtip2 (~> 2.2.0)! - rails-assets-selectize (~> 0.11.2)! - rails-assets-spinjs (~> 2.0.0)! - rails_stdout_logging (~> 0.0.3) - rollbar (~> 2.10.0) - rspec-html-matchers (~> 0.5.0) - rspec-rails (~> 2.99.0) - rspec_junit_formatter (~> 0.2.3) - rubocop (~> 0.27.1) + rabl (~> 0.13.0) + rack-proxy (~> 0.6.0) + rack-timeout (~> 0.4.2) + rails (~> 4.2.7.1) + rails_stdout_logging (~> 0.0.5) + request_store (~> 1.3.1) + rollbar (~> 2.12.0) safe_yaml (~> 1.0.4) - sass (~> 3.2.19) - sass-rails (~> 3.2.6) seed-fu! - sequel (~> 4.31.0) - sprockets-urlrewriter (~> 0.1.2) - tabs_on_rails (~> 2.2.0) - test-unit - therubyracer (~> 0.12.2) - turbo-sprockets-rails3 (= 0.3.13) - uglifier (~> 2.7.1) - uuidtools (~> 2.1.4) - vcr (~> 2.9.3) - webmock (~> 1.21.0) + sequel (~> 4.37.0) BUNDLED WITH - 1.12.5 + 1.13.6 diff --git a/src/api-umbrella/web-app/Rakefile b/src/api-umbrella/web-app/Rakefile index 9907c6e10..9191afc6b 100644 --- a/src/api-umbrella/web-app/Rakefile +++ b/src/api-umbrella/web-app/Rakefile @@ -1,10 +1,9 @@ -#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -ApiUmbrella::Application.load_tasks +Rails.application.load_tasks task(:default).clear -task :default => [:rubocop, :jshint, :spec] +task :default => [:rubocop, :spec] diff --git a/src/api-umbrella/web-app/app/assets/images/icon/arrow-blue.png b/src/api-umbrella/web-app/app/assets/images/icon/arrow-blue.png deleted file mode 100644 index deca54f83..000000000 Binary files a/src/api-umbrella/web-app/app/assets/images/icon/arrow-blue.png and /dev/null differ diff --git a/src/api-umbrella/web-app/app/assets/images/icon/arrow-orange.png b/src/api-umbrella/web-app/app/assets/images/icon/arrow-orange.png deleted file mode 100644 index dcaf8c31f..000000000 Binary files a/src/api-umbrella/web-app/app/assets/images/icon/arrow-orange.png and /dev/null differ diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin.js b/src/api-umbrella/web-app/app/assets/javascripts/admin.js deleted file mode 100644 index 66dc68ea1..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin.js +++ /dev/null @@ -1,118 +0,0 @@ -//= require jquery/jquery.js -//= require jquery_ujs -//= require bootstrap -//= require ace-builds/src/ace -//= require ace-builds/src/mode-json -//= require ace-builds/src/mode-xml -//= require ace-builds/src/mode-yaml -//= require handlebars -//= require ember -//= require qtip2 -//= require lodash/lodash.compat -//= require datatables/jquery.dataTables.js -//= require vendor/datatables-plugins/dataTables.bootstrap -//= require jsdiff/diff -//= require ic-ajax/dist/globals/main.js -//= require vendor/ember-model/ember-model.js -//= require vendor/ember-easyForm -//= require vendor/ember-validations -//= require marked -//= require pnotify -//= require bootbox -//= require jquery.scrollTo -//= require vendor/jquery-ui-1.10.3.custom -//= require jquery-bbq-deparam -//= require selectize/standalone/selectize -//= require inflection -//= require jstz-detect/jstz -//= require vendor/jquery.slugify -//= require moment -//= require bootstrap-daterangepicker -//= require numeral -//= require vendor/jquery.blockUI -//= require vendor/jQuery-QueryBuilder/query-builder.standalone.js -//= require spinjs -//= require vendor/dirtyforms/jquery.dirtyforms -//= require vendor/jquery.truncate -//= require admin/app -//= require_self - -$(document).ready(function() { - // Use the default browser "beforeunload" dialog. - $.DirtyForms.dialog = false; - $(window).bind('beforeunload', function() { - if($.DirtyForms.isDirty()) { - return $.DirtyForms.message; - } else { - return; - } - }); - - $('form').dirtyForms(); - - // Setup qTip defaults. - $(document).on('click', 'a[rel=tooltip]', function(event) { - $(this).qtip({ - overwrite: false, - show: { - event: event.type, - ready: true, - solo: true - }, - hide: { - event: 'unfocus' - }, - style: { - classes: 'qtip-bootstrap ' + $(this).data('tooltip-class'), - }, - position: { - viewport: true, - my: 'bottom left', - at: 'top center', - adjust: { - y: 2 - } - } - }, event); - - event.preventDefault(); - }); - - $.blockUI.defaults.fadeIn = 0; - $.blockUI.defaults.fadeOut = 0; - - $(document).on('click', 'a[rel=popover]', function(event) { - $(this).qtip({ - overwrite: false, - show: { - event: event.type, - ready: true, - solo: true - }, - hide: { - event: 'unfocus' - }, - content: { - text: function(event) { - var target = $(event.target).attr('href'); - var content = $(target).html(); - - return content; - }, - }, - style: { - classes: 'qtip-bootstrap qtip-wide', - }, - position: { - viewport: false, - my: 'top left', - at: 'bottom center', - adjust: { - y: 2 - } - } - }, event); - - event.preventDefault(); - }); -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/_common_validations.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/_common_validations.js.erb new file mode 100644 index 000000000..51c2c9283 --- /dev/null +++ b/src/api-umbrella/web-app/app/assets/javascripts/admin/_common_validations.js.erb @@ -0,0 +1,5 @@ +var CommonValidations = { + host_format: new RegExp(<%= CommonValidations.to_js(CommonValidations::HOST_FORMAT).to_json %>), + host_format_with_wildcard: new RegExp(<%= CommonValidations.to_js(CommonValidations::HOST_FORMAT_WITH_WILDCARD).to_json %>), + url_prefix_format: new RegExp(<%= CommonValidations.to_js(CommonValidations::URL_PREFIX_FORMAT).to_json %>) +}; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/app.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/app.js deleted file mode 100644 index f4add6d46..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/app.js +++ /dev/null @@ -1,512 +0,0 @@ -//= require_self -//= require ./common_validations -//= require_tree ./models -//= require ./controllers/apis/nested_form_controller -//= require ./controllers/apis/sortable_controller -//= require_tree ./components -//= require_tree ./controllers -//= require_tree ./views -//= require_tree ./helpers -//= require_tree ./templates -//= require ./router -//= require_tree ./routes - -//Ember.LOG_BINDINGS = true - -// Set Bootbox defaults. -bootbox.animate(false); - -// PNotify Defaults. -_.merge(PNotify.prototype.options, { - styling: 'bootstrap2', - width: '400px', - icon: false, - animate_speed: 'fast', - history: { - history: false - }, - buttons: { - sticker: false - } -}); - -(function() { - var versionParts = Ember.VERSION.split('.'); - var major = parseInt(versionParts[0], 10); - var minor = parseInt(versionParts[1], 10); - var patch = parseInt(versionParts[2], 10); - if(major > 1 || (major === 1 && (minor > 1 || patch > 2))) { - Ember.Logger.warn('WARNING: New Ember version detected. URL hash monkey patch possibly no longer needed or broken. Check for compatibility.'); - } - - var get = Ember.get, set = Ember.set; - - // Fix URL hash parsing across browsers. Because we're putting query - // parameters in the URL, we expect special characters which escape - // differently across browsers with location.hash. So this is a monkey patch - // to use location.href instead. - // https://github.com/emberjs/ember.js/issues/3000 - // https://github.com/emberjs/ember.js/issues/3263 - // - // All of this should be revisited once query-params lands, since this will - // probably clean this up: https://github.com/emberjs/ember.js/pull/3182 - Ember.HashLocation.reopen({ - getURL: function() { - var href = get(this, 'location').href; - - var hashIndex = href.indexOf('#'); - if (hashIndex === -1) { - return ''; - } else { - return href.substr(hashIndex + 1); - } - }, - - onUpdateURL: function(callback) { - var self = this; - var guid = Ember.guidFor(this); - - Ember.$(window).on('hashchange.ember-location-'+guid, function() { - Ember.run(function() { - var path = self.getURL(); - if (get(self, 'lastSetURL') === path) { return; } - - set(self, 'lastSetURL', null); - - callback(path); - }); - }); - }, - }); -})(); - -window.Admin = Ember.Application.create({ - LOG_TRANSITIONS: true, - LOG_TRANSITIONS_INTERNAL: true, - - rootElement: '#content' -}); - -function eachTranslatedAttribute(object, fn) { - var isTranslatedAttribute = /(.+)Translation$/, - isTranslatedAttributeMatch; - - for (var key in object) { - isTranslatedAttributeMatch = key.match(isTranslatedAttribute); - if (isTranslatedAttributeMatch) { - var translation = (!object[key]) ? null : polyglot.t(object[key]); - fn.call(object, isTranslatedAttributeMatch[1], translation); - } - } -} - -// Override existing Ember.EasyForm.processOptions to use our polyglot -// translations instead of Ember.i18n for the special *Translation fields. -// -// We could also potentially use subexpressions to call polyglot directly in -// the templates, but at least as of Ember 1.7, there are bugs with multiple -// subexpressions: https://github.com/wycats/handlebars.js/issues/748 -// Perhaps revisit when we upgrade Ember. -Ember.EasyForm.processOptions = function(property, options) { - if(options) { - if(polyglot) { - eachTranslatedAttribute(options.hash, function(attribute, translation) { - options.hash[attribute] = translation; - delete options.hash[attribute + 'Translation']; - }); - } - options.hash.property = property; - } else { - options = property; - } - - return options; -}; - -Ember.EasyForm.Tooltip = Ember.EasyForm.BaseView.extend({ - tagName: 'a', - attributeBindings: ['title', 'rel', 'data-tooltip-class'], - template: Ember.Handlebars.compile(''), - rel: 'tooltip', -}); - -Ember.Handlebars.registerBoundHelper('formatDate', function(date, format) { - if(!format || !_.isString(format)) { - format = 'YYYY-MM-DD HH:mm Z'; - } - - if(date) { - return moment(date).format(format); - } else { - return ''; - } -}); - -Ember.Handlebars.helper('formatNumber', function(number) { - return numeral(number).format('0,0'); -}); - -Ember.Handlebars.helper('inflect', function(word, number) { - return inflection.inflect(word, number); -}); - -// i18n helper via polyglot library -Ember.Handlebars.registerHelper('t', function(property, options) { - return polyglot.t(property, options.hash); -}); - -Ember.Handlebars.registerHelper('tooltip-field', function(property, options) { - options = Ember.EasyForm.processOptions(property, options); - options.hash.viewName = 'tooltip-field-'+options.data.view.elementId; - return Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Tooltip, options); -}); - -// Use a custom template for Easy Form. This adds a tooltip and wraps that in -// the control-label div with the label. -Ember.TEMPLATES['easyForm/wrapped_input'] = Ember.Handlebars.compile('
{{label-field propertyBinding="view.property" textBinding="view.label" viewBinding="view"}}{{#if view.tooltip}}{{tooltip-field titleBinding="view.tooltip" data-tooltip-classBinding="view.tooltipClass"}}{{/if}}
{{partial "easyForm/inputControls"}}
'); - -Ember.EasyForm.Config.registerInputType('selectize', Ember.EasyForm.TextField.extend({ - defaultOptions: [], - - init: function() { - this._super(); - this.set('selectizeTextInputId', this.elementId + '_selectize_text_input'); - this.set('overrideForElementId', this.get('selectizeTextInputId')); - }, - - didInsertElement: function() { - this._super(); - - this.$input = this.$().selectize({ - plugins: ['restore_on_backspace', 'remove_button'], - delimiter: ',', - options: this.get('defaultOptions'), - valueField: 'id', - labelField: 'label', - searchField: 'label', - sortField: 'label', - onChange: _.bind(this.handleSelectizeChange, this), - create: true, - - // Add to body so it doesn't get clipped by parent div containers. - dropdownParent: 'body', - }); - - this.selectize = this.$input[0].selectize; - this.selectize.$control_input.attr('id', this.get('selectizeTextInputId')); - this.selectize.$control_input.attr('data-raw-input-id', this.elementId); - - var controlId = this.elementId + '_selectize_control'; - this.selectize.$control.attr('id', controlId); - this.selectize.$control_input.attr('data-selectize-control-id', controlId); - }, - - defaultOptionsDidChange: function() { - this.set('defaultOptions', this.get('collection').map(_.bind(function(item) { - return { - id: item.get(this.get('optionValuePath')), - label: item.get(this.get('optionLabelPath')), - }; - }, this))); - - if(this.selectize) { - this.get('defaultOptions').forEach(_.bind(function(option) { - this.selectize.addOption(option); - }, this)); - - this.selectize.refreshOptions(false); - } - }.observes('collection.@each').on('init'), - - // Sync the selectize input with the value binding if the value changes - // externally. - valueDidChange: function() { - if(this.selectize) { - var valueString = this.get('value'); - if(valueString !== this.selectize.getValue()) { - var values = valueString; - if(values) { - values = _.uniq(values.split(',')); - - // Ensure the selected value is available as an option in the menu. - // This takes into account the fact that the default options may not - // be loaded yet, or they may not contain this specific option. - for(var i = 0; i < values.length; i++) { - var option = { - id: values[i], - label: values[i], - }; - - this.selectize.addOption(option); - } - - this.selectize.refreshOptions(false); - } - - this.selectize.setValue(values); - } - } - }.observes('value').on('init'), - - // Update the value binding when the selectize input changes. - handleSelectizeChange: function(value) { - this.set('value', value); - }, - - willDestroyElement: function() { - if(this.selectize) { - this.selectize.destroy(); - } - }, -})); - -Ember.EasyForm.Config.registerInputType('ace', Ember.EasyForm.TextArea.extend({ - attributeBindings: ['data-ace-mode'], - - init: function() { - this._super(); - this.set('aceId', this.elementId + '_ace'); - this.set('aceTextInputId', this.elementId + '_ace_text_input'); - this.set('overrideForElementId', this.get('aceTextInputId')); - }, - - didInsertElement: function() { - this._super(); - - var aceId = this.get('aceId'); - this.$().hide(); - this.$().before('
'); - - this.editor = ace.edit(aceId); - - var editor = this.editor; - var session = this.editor.getSession(); - var element = this.$(); - - editor.setTheme('ace/theme/textmate'); - editor.setShowPrintMargin(false); - editor.setHighlightActiveLine(false); - session.setUseWorker(false); - session.setTabSize(2); - session.setMode('ace/mode/' + this.$().data('ace-mode')); - session.setValue(this.$().val()); - - var $textElement = $(editor.textInput.getElement()); - $textElement.attr('id', this.get('aceTextInputId')); - $textElement.attr('data-raw-input-id', this.elementId); - - var contentId = this.elementId + '_ace_content'; - var $content = $(editor.container).find('.ace_content'); - $content.attr('id', contentId); - $textElement.attr('data-ace-content-id', contentId); - - - session.on('change', function() { - element.val(session.getValue()); - element.trigger('change'); - }); - }, -})); - -Ember.EasyForm.Config.registerWrapper('default', { - formClass: '', - fieldErrorClass: 'error', - errorClass: 'help-block', - hintClass: 'help-block', - inputClass: 'control-group', - wrapControls: true, - controlsWrapperClass: 'controls' -}); - -Admin.APIUmbrellaRESTAdapter = Ember.RESTAdapter.extend({ - ajaxSettings: function(url, method) { - return { - url: url, - type: method, - dataType: 'json', - headers: { - 'X-Api-Key': webAdminAjaxApiKey - } - }; - } -}); - -$.ajaxPrefilter(function(options) { - options.headers = options.headers || {}; - options.headers['X-Api-Key'] = webAdminAjaxApiKey; -}); - -// DataTables plugin to programmatically show the processing indidicator. -// https://datatables.net/plug-ins/api#fnProcessingIndicator -jQuery.fn.dataTableExt.oApi.fnProcessingIndicator = function ( oSettings, onoff ) -{ - if( typeof(onoff) === 'undefined' ) - { - onoff=true; - } - this.oApi._fnProcessingDisplay( oSettings, onoff ); -}; - - -// Defaults for DataTables. -_.merge($.fn.DataTable.defaults, { - // Don't show the DataTables processing message. We'll handle the processing - // message logic in initComplete with blockui. - processing: false, - - // Enable global searching. - searching: true, - - // Re-arrange how the table and surrounding fields (pagination, search, etc) - // are laid out. - dom: 'rft<"row-fluid"<"span3 table-info"i><"span6 table-pagination"p><"span3 table-length"l>>', - - language: { - // Don't have an explicit label for the search field. Use a placeholder - // instead. - search: '', - searchPlaceholder: 'Search...', - }, - - preDrawCallback: function() { - if(!this.customProcessingCallbackSet) { - // Use blockui to provide a more obvious processing message the overlays - // the entire table (this helps for long tables, where a simple processing - // message might appear out of your current view). - // - // Set this early on during pre-draw so that the processing message shows - // up for the first load. - $(this).DataTable().on('processing', _.bind(function(event, settings, processing) { - if(processing) { - this.block({ - message: '', - }); - } else { - this.unblock(); - } - }, this)); - - this.customProcessingCallbackSet = true; - } - }, -}); - -Ember.EasyForm.Input.reopen({ - didInsertElement: function() { - var forId = this.get('input-field-' + this.elementId + '.overrideForElementId') || this.get('input-field-' + this.elementId + '.elementId'); - this.set('label-field-' + this.elementId + '.for', forId); - }, - - // Observe the "showAllValidationErrors" property and show all the inline - // input validations when this gets set to true. This allows us to show all - // the invalid fields on the page without actually visiting each input field - // (useful on form submits). This is a bit of a workaround since - // ember-easyForm doesn't currently support this: - // https://github.com/dockyard/ember-easyForm/issues/146 - // https://github.com/dockyard/ember-easyForm/pull/143 - showAllValidationErrorsOnModelChange: function() { - if(this.get('context.showAllValidationErrors') === true) { - this.set('hasFocusedOut', true); - this.set('canShowValidationError', true); - } else { - this.showValidationError(); - } - }.observes('context.showAllValidationErrors'), -}); - -Ember.EasyForm.Form.reopen({ - submit: function(event) { - if (event) { - event.preventDefault(); - } - - if(!this.get('context.model.validate')) { - this.get('controller').send(this.get('action')); - } else { - // Reset the error objects used for error-messages display before each - // submit, so the messages reflect the new validations. - this.set('context.model.clientErrors', {}); - this.set('context.model.serverErrors', {}); - - this.get('context.model').validate().then(_.bind(function() { - this.get('controller').send(this.get('action')); - }, this)).catch(_.bind(function() { - // On validation failure, set the errors for error-messages display and - // scroll to the error messages display. - this.set('context.model.clientErrors', this.get('context.model.errors')); - $.scrollTo('#error_messages', { offset: -50, duration: 200 }); - - // Display all the inline errors for at least the top-level model - // (note, this doesn't currently propagate to embedded models/forms). - this.set('context.model.showAllValidationErrors', true); - }, this)); - } - }, -}); - -// A mixin that provides the default ajax save behavior for our forms. -Admin.Save = Ember.Mixin.create({ - save: function(options) { - var button = $('#save_button'); - button.button('loading'); - - // Force dirty to force save (ember-model's dirty tracking fails to - // account for changes in nested, non-association objects: - // http://git.io/sbS1mg This is mainly for ApiSettings's errorTemplates - // and errorDataYamlStrings, but we've seen enough funkiness elsewhere, - // it seems worth disabling for now). - this.set('model.isDirty', true); - - this.get('model').save().then(_.bind(function() { - button.button('reset'); - new PNotify({ - type: 'success', - title: 'Saved', - text: (_.isFunction(options.message)) ? options.message(this.get('model')) : options.message, - }); - - this.transitionToRoute(options.transitionToRoute); - }, this), _.bind(function(response) { - // Set the errors from the server response on a "serverErrors" property - // for the error-messages component display. - try { - this.set('model.serverErrors', response.responseJSON.errors); - } catch(e) { - this.set('model.serverErrors', response.responseText); - } - - button.button('reset'); - $.scrollTo('#error_messages', { offset: -50, duration: 200 }); - }, this)); - }, -}); - -Admin.DataTablesHelpers = { - renderEscaped: function(value, type) { - if(type === 'display' && value) { - return _.escape(value); - } - - return value; - }, - - renderListEscaped: function(value, type) { - if(type === 'display' && value) { - if(_.isArray(value)) { - return _.map(value, function(v) { return _.escape(v); }).join('
'); - } else { - return _.escape(value); - } - } - - return value; - }, - - renderTime: function(value, type) { - if(type === 'display' && value && value !== '-') { - return moment(value).format('YYYY-MM-DD HH:mm:ss'); - } - - return value; - }, -}; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/common_validations.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/common_validations.js.erb deleted file mode 100644 index 16bea1379..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/common_validations.js.erb +++ /dev/null @@ -1,5 +0,0 @@ -var CommonValidations = { - host_format: new RegExp(<%= CommonValidations::HOST_FORMAT.source.to_json %>), - host_format_with_wildcard: new RegExp(<%= CommonValidations::HOST_FORMAT_WITH_WILDCARD.source.to_json %>), - url_prefix_format: new RegExp(<%= CommonValidations::URL_PREFIX_FORMAT.source.to_json %>) -}; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/components/api-settings-fields.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/components/api-settings-fields.js deleted file mode 100644 index be1bcf9c2..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/components/api-settings-fields.js +++ /dev/null @@ -1,29 +0,0 @@ -Admin.ApiSettingsFieldsComponent = Ember.Component.extend({ - requireHttpsOptions: [ - { id: null, name: polyglot.t('admin.api.settings.require_https_options.inherit') }, - { id: 'required_return_error', name: polyglot.t('admin.api.settings.require_https_options.required_return_error') }, - { id: 'required_return_redirect', name: polyglot.t('admin.api.settings.require_https_options.required_return_redirect') }, - { id: 'transition_return_error', name: polyglot.t('admin.api.settings.require_https_options.transition_return_error') }, - { id: 'transition_return_redirect', name: polyglot.t('admin.api.settings.require_https_options.transition_return_redirect') }, - { id: 'optional', name: polyglot.t('admin.api.settings.require_https_options.optional') }, - ], - - disableApiKeyOptions: [ - { id: null, name: polyglot.t('admin.api.settings.disable_api_key_options.inherit') }, - { id: false, name: polyglot.t('admin.api.settings.disable_api_key_options.required') }, - { id: true, name: polyglot.t('admin.api.settings.disable_api_key_options.disabled') }, - ], - - apiKeyVerificationLevelOptions: [ - { id: null, name: polyglot.t('admin.api.settings.api_key_verification_level_options.inherit') }, - { id: 'none', name: polyglot.t('admin.api.settings.api_key_verification_level_options.none') }, - { id: 'transition_email', name: polyglot.t('admin.api.settings.api_key_verification_level_options.transition_email') }, - { id: 'required_email', name: polyglot.t('admin.api.settings.api_key_verification_level_options.required_email') }, - ], - - roleOptions: function() { - return Admin.ApiUserRole.find(); - // Don't cache this property, so we can rely on refreshing the underlying - // model to refresh the options. - }.property().cacheable(false), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/components/error_messages_component.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/components/error_messages_component.js deleted file mode 100644 index 4a3d58dfb..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/components/error_messages_component.js +++ /dev/null @@ -1,48 +0,0 @@ -Admin.ErrorMessagesComponent = Ember.Component.extend({ - messages: function() { - var messages = []; - - var errors = _.extend({}, this.get('model.clientErrors')); - - var serverErrors = this.get('model.serverErrors'); - if(serverErrors) { - if(_.isString(serverErrors)) { - messages.push(serverErrors); - } else if(_.isArray(serverErrors)) { - _.each(serverErrors, function(serverError) { - var field = 'base'; - var message = serverError; - if(_.isObject(serverError)) { - if(serverError.field) { - field = serverError.field; - } - - message = serverError.message; - } - - if(!errors[field]) { - errors[field] = []; - } - - errors[field].push(message); - }); - } else { - errors = _.merge(errors, serverErrors); - } - } - - _.forOwn(errors, function(attrErrors, attr) { - _.each(attrErrors, function(attrError) { - var message = ''; - if(attr !== 'base') { - message += inflection.titleize(inflection.underscore(attr)) + ': '; - } - message += attrError; - - messages.push(marked(message)); - }); - }); - - return messages; - }.property('model.clientErrors', 'model.serverErrors'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admin_groups/form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admin_groups/form_controller.js deleted file mode 100644 index 93f7d6fdb..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admin_groups/form_controller.js +++ /dev/null @@ -1,30 +0,0 @@ -Admin.AdminGroupsFormController = Ember.ObjectController.extend(Admin.Save, { - apiScopeOptions: function() { - return Admin.ApiScope.find(); - }.property(), - - permissionOptions: function() { - return Admin.AdminPermission.find(); - }.property(), - - actions: { - submit: function() { - this.save({ - transitionToRoute: 'admin_groups', - message: 'Successfully saved the admin group "' + _.escape(this.get('model.name')) + '"', - }); - }, - - delete: function() { - bootbox.confirm('Are you sure you want to delete this admin group?', _.bind(function(result) { - if(result) { - this.get('model').deleteRecord(); - this.transitionToRoute('admin_groups'); - } - }, this)); - }, - }, -}); - -Admin.AdminGroupsEditController = Admin.AdminGroupsFormController.extend(); -Admin.AdminGroupsNewController = Admin.AdminGroupsFormController.extend(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admins/form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admins/form_controller.js deleted file mode 100644 index a5560656a..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admins/form_controller.js +++ /dev/null @@ -1,30 +0,0 @@ -Admin.AdminsFormController = Ember.ObjectController.extend(Admin.Save, { - groupOptions: function() { - return Admin.AdminGroup.find(); - }.property(), - - currentAdmin: function() { - return currentAdmin; - }.property(), - - actions: { - submit: function() { - this.save({ - transitionToRoute: 'admins', - message: 'Successfully saved the admin "' + _.escape(this.get('model.username')) + '"', - }); - }, - - delete: function() { - bootbox.confirm('Are you sure you want to delete this admin?', _.bind(function(result) { - if(result) { - this.get('model').deleteRecord(); - this.transitionToRoute('admins'); - } - }, this)); - }, - }, -}); - -Admin.AdminsEditController = Admin.AdminsFormController.extend(); -Admin.AdminsNewController = Admin.AdminsFormController.extend(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admins/index_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admins/index_controller.js deleted file mode 100644 index 6912cfeed..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/admins/index_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -Admin.AdminsIndexController = Ember.ObjectController.extend({ - queryParams: null, - - downloadUrl: function() { - return '/api-umbrella/v1/admins.csv?' + $.param(this.get('queryParams')) + '&api_key=' + webAdminAjaxApiKey; - }.property('queryParams'), - - actions: { - paramsChange: function(newParams) { - // Remove paging - delete newParams.start; - delete newParams.length; - this.set('queryParams', newParams); - } - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/api_scopes/form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/api_scopes/form_controller.js deleted file mode 100644 index 0a424aba3..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/api_scopes/form_controller.js +++ /dev/null @@ -1,22 +0,0 @@ -Admin.ApiScopesFormController = Ember.ObjectController.extend(Admin.Save, { - actions: { - submit: function() { - this.save({ - transitionToRoute: 'api_scopes', - message: 'Successfully saved the API scope "' + _.escape(this.get('model.name')) + '"', - }); - }, - - delete: function() { - bootbox.confirm('Are you sure you want to delete this API scope?', _.bind(function(result) { - if(result) { - this.get('model').deleteRecord(); - this.transitionToRoute('api_scopes'); - } - }, this)); - }, - }, -}); - -Admin.ApiScopesEditController = Admin.ApiScopesFormController.extend(); -Admin.ApiScopesNewController = Admin.ApiScopesFormController.extend(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/api_users/form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/api_users/form_controller.js deleted file mode 100644 index a87ef3e8e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/api_users/form_controller.js +++ /dev/null @@ -1,36 +0,0 @@ -Admin.ApiUsersFormController = Ember.ObjectController.extend(Admin.Save, { - throttleByIpOptions: [ - { id: false, name: 'Rate limit by API key' }, - { id: true, name: 'Rate limit by IP address' }, - ], - - enabledOptions: [ - { id: true, name: 'Enabled' }, - { id: false, name: 'Disabled' }, - ], - - roleOptions: function() { - return Admin.ApiUserRole.find(); - // Don't cache this property, so we can rely on refreshing the underlying - // model to refresh the options. - }.property().cacheable(false), - - actions: { - submit: function() { - this.save({ - transitionToRoute: 'api_users', - message: function(model) { - var message = 'Successfully saved the user "' + _.escape(model.get('email')) + '"'; - if(model.get('apiKey')) { - message += '
API Key: ' + _.escape(model.get('apiKey')) + ''; - } - - return message; - }, - }); - }, - }, -}); - -Admin.ApiUsersEditController = Admin.ApiUsersFormController.extend(); -Admin.ApiUsersNewController = Admin.ApiUsersFormController.extend(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/nested_form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/nested_form_controller.js deleted file mode 100644 index 44d952ad1..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/nested_form_controller.js +++ /dev/null @@ -1,66 +0,0 @@ -Admin.NestedFormController = Ember.ObjectController.extend(Ember.Evented, { - needs: ['modal'], - - titleBase: null, - apiModel: null, - parentCollection: null, - isNew: null, - originalData: null, - - setup: function(apiModel, parentCollectionName) { - this.set('apiModel', apiModel); - this.set('parentCollection', apiModel.get(parentCollectionName)); - }, - - add: function(apiModel, parentCollectionName) { - this.setup(apiModel, parentCollectionName); - this.set('model', this.get('parentCollection').create()); - - this.set('controllers.modal.title', 'Add ' + this.get('titleBase')); - this.set('isNew', true); - }, - - edit: function(apiModel, parentCollectionName, record) { - this.setup(apiModel, parentCollectionName); - this.set('model', record); - - this.set('controllers.modal.title', 'Edit ' + this.get('titleBase')); - this.set('isNew', false); - this.set('originalData', record.toJSON()); - }, - - actions: { - ok: function() { - var model = this.get('model'); - - // Validate the nested model (if supported) before allowing the modal to - // close. - if(model.validate) { - model.validate().then(_.bind(function() { - this.send('closeModal'); - - // Fire a "closeOk" event other things can listen for to determine - // when the form content inside the modal may have successfully - // changed. - this.trigger('closeOk'); - }, this)).catch(function() { - model.set('showAllValidationErrors', true); - }); - } else { - this.send('closeModal'); - this.trigger('closeOk'); - } - }, - - cancel: function() { - if(this.get('isNew')) { - this.get('parentCollection').removeObject(this.get('model')); - } else { - var data = this.get('originalData'); - this.get('model').load(data.id, data); - } - - this.send('closeModal'); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/rewrite_form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/rewrite_form_controller.js deleted file mode 100644 index 1c68b9ac3..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/rewrite_form_controller.js +++ /dev/null @@ -1,21 +0,0 @@ -Admin.ApisRewriteFormController = Admin.NestedFormController.extend({ - titleBase: 'Rewrite', - - matcherTypeOptions: [ - { id: 'route', name: 'Route Pattern' }, - { id: 'regex', name: 'Regular Expression' }, - ], - - httpMethodOptions: [ - { id: 'any', name: 'Any' }, - { id: 'GET', name: 'GET' }, - { id: 'POST', name: 'POST' }, - { id: 'PUT', name: 'PUT' }, - { id: 'DELETE', name: 'DELETE' }, - { id: 'HEAD', name: 'HEAD' }, - { id: 'TRACE', name: 'TRACE' }, - { id: 'OPTIONS', name: 'OPTIONS' }, - { id: 'CONNECT', name: 'CONNECT' }, - { id: 'PATCH', name: 'PATCH' }, - ], -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/rewrites_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/rewrites_controller.js deleted file mode 100644 index 8a6604668..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/rewrites_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -Admin.ApisRewritesController = Admin.ApisSortableController.extend({ - actions: { - reorderRewrites: function() { - this.reorderCollection('rewrites'); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/server_form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/server_form_controller.js deleted file mode 100644 index 99fc80623..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/server_form_controller.js +++ /dev/null @@ -1,3 +0,0 @@ -Admin.ApisServerFormController = Admin.NestedFormController.extend({ - titleBase: 'Server', -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/settings_rate_limit_fields_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/settings_rate_limit_fields_controller.js deleted file mode 100644 index c77a013b4..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/settings_rate_limit_fields_controller.js +++ /dev/null @@ -1,48 +0,0 @@ -Admin.ApisSettingsRateLimitFieldsController = Ember.ObjectController.extend({ - rateLimitModeOptions: [ - { id: null, name: 'Default rate limits' }, - { id: 'custom', name: 'Custom rate limits' }, - { id: 'unlimited', name: 'Unlimited requests' }, - ], - - rateLimitDurationUnitOptions: [ - { id: 'seconds', name: 'seconds' }, - { id: 'minutes', name: 'minutes' }, - { id: 'hours', name: 'hours' }, - { id: 'days', name: 'days' }, - ], - - rateLimitLimitByOptions: [ - { id: 'apiKey', name: 'API Key' }, - { id: 'ip', name: 'IP Address' }, - ], - - anonymousRateLimitBehaviorOptions: [ - { id: 'ip_fallback', name: 'IP Fallback - API key rate limits are applied as IP limits' }, - { id: 'ip_only', name: 'IP Only - API key rate limits are ignored (only IP based limits are applied)' }, - ], - - authenticatedRateLimitBehaviorOptions: [ - { id: 'all', name: 'All Limits - Both API key rate limits and IP based limits are applied' }, - { id: 'api_key_only', name: 'API Key Only - IP based rate limits are ignored (only API key limits are applied)' }, - ], - - uniqueSettingsId: function() { - return _.uniqueId('api_settings_'); - }.property(), - - actions: { - addRateLimit: function() { - this.get('model.rateLimits').create(); - }, - - deleteRateLimit: function(rateLimit) { - var collection = this.get('model.rateLimits'); - bootbox.confirm('Are you sure you want to remove this rate limit?', function(result) { - if(result) { - collection.removeObject(rateLimit); - } - }); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sortable_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sortable_controller.js deleted file mode 100644 index 5f2ee1593..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sortable_controller.js +++ /dev/null @@ -1,47 +0,0 @@ -Admin.ApisSortableController = Ember.ArrayController.extend({ - reorderable: function() { - var length = this.get('length'); - return (length && length > 1); - }.property('length'), - - updateSortOrder: function(indexes) { - this.forEach(function(record) { - var index = indexes[record.get('id')]; - record.set('sortOrder', index); - }); - }, - - reorderCollection: function(containerId) { - var $container = $('#' + containerId); - var $buttonText = $container.find('.reorder-button-text'); - - if($container.hasClass('reorder-active')) { - $buttonText.text($buttonText.data('originalText')); - } else { - $buttonText.data('originalText', $buttonText.text()); - $buttonText.text('Done'); - } - - $container.toggleClass('reorder-active'); - - var controller = this; - $container.find('tbody').sortable({ - handle: '.reorder-handle', - placeholder: 'reorder-placeholder', - helper: function(event, ui) { - ui.children().each(function() { - $(this).width($(this).width()); - }); - return ui; - }, - stop: function() { - var indexes = {}; - $(this).find('tr').each(function(index) { - indexes[$(this).data('id')] = index; - }); - - controller.updateSortOrder(indexes); - }, - }); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sub_settings_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sub_settings_controller.js deleted file mode 100644 index 1e65653ec..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sub_settings_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -Admin.ApisSubSettingsController = Admin.ApisSortableController.extend({ - actions: { - reorderSubSettings: function() { - this.reorderCollection('sub_settings'); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sub_settings_form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sub_settings_form_controller.js deleted file mode 100644 index d1aa13986..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/sub_settings_form_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -Admin.ApisSubSettingsFormController = Admin.NestedFormController.extend({ - titleBase: 'Sub-URL Request Settings', - - httpMethodOptions: [ - { id: 'any', name: 'Any' }, - { id: 'GET', name: 'GET' }, - { id: 'POST', name: 'POST' }, - { id: 'PUT', name: 'PUT' }, - { id: 'DELETE', name: 'DELETE' }, - { id: 'HEAD', name: 'HEAD' }, - { id: 'TRACE', name: 'TRACE' }, - { id: 'OPTIONS', name: 'OPTIONS' }, - { id: 'CONNECT', name: 'CONNECT' }, - { id: 'PATCH', name: 'PATCH' }, - ], -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/url_match_form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/url_match_form_controller.js deleted file mode 100644 index 0de0262f5..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/url_match_form_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -Admin.ApisUrlMatchFormController = Admin.NestedFormController.extend({ - titleBase: 'Matching URL Prefix', - exampleSuffix: 'example.json?param=value', - - exampleIncomingUrl: function() { - var root = this.get('apiModel.exampleIncomingUrlRoot') || ''; - var prefix = this.get('frontendPrefix') || ''; - return root + prefix + this.get('exampleSuffix'); - }.property('frontendPrefix'), - - exampleOutgoingUrl: function() { - var root = this.get('apiModel.exampleOutgoingUrlRoot') || ''; - var prefix = this.get('backendPrefixWithDefault') || ''; - return root + prefix + this.get('exampleSuffix'); - }.property('backendPrefix', 'frontendPrefix'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/url_matches_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/url_matches_controller.js deleted file mode 100644 index acc3bfa49..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis/url_matches_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -Admin.ApisUrlMatchesController = Admin.ApisSortableController.extend({ - actions: { - reorderUrlMatches: function() { - this.reorderCollection('url_matches'); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_form_controller.js deleted file mode 100644 index ae646dd99..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_form_controller.js +++ /dev/null @@ -1,124 +0,0 @@ -Admin.ApisFormController = Ember.ObjectController.extend(Admin.Save, { - needs: [ - 'apis_server_form', - 'apis_url_match_form', - 'apis_sub_settings_form', - 'apis_rewrite_form', - ], - - backendProtocolOptions: [ - { id: 'http', name: 'http' }, - { id: 'https', name: 'https' }, - ], - - balanceAlgorithmOptions: [ - { id: 'least_conn', name: 'Least Connections' }, - { id: 'round_robin', name: 'Round Robin' }, - { id: 'ip_hash', name: 'Source IP Hash' }, - ], - - actions: { - submit: function() { - this.save({ - transitionToRoute: 'apis', - message: 'Successfully saved the "' + _.escape(this.get('model.name')) + '" API backend
Note: Your changes are not yet live. Publish Changes to send your updates live.', - }); - }, - - delete: function() { - bootbox.confirm('Are you sure you want to delete this API backend?', _.bind(function(result) { - if(result) { - this.get('model').deleteRecord(); - this.transitionToRoute('apis'); - } - }, this)); - }, - - addServer: function() { - this.get('controllers.apis_server_form').add(this.get('model'), 'servers'); - - // For new servers, intelligently pick the default port based on the - // backend protocol selected. - if(this.get('model.backendProtocol') === 'https') { - this.set('controllers.apis_server_form.model.port', 443); - } else { - this.set('controllers.apis_server_form.model.port', 80); - } - - // After the first server is added, fill out a default value for the - // "Backend Host" field based on the server's host (because in most - // non-load balancing situations they will match). - this.get('controllers.apis_server_form').on('closeOk', _.bind(function() { - var server = this.get('model.servers.firstObject'); - if(!this.get('model.backendHost') && server) { - this.set('model.backendHost', server.get('host')); - } - }, this)); - - this.send('openModal', 'apis/server_form'); - }, - - editServer: function(server) { - this.get('controllers.apis_server_form').edit(this.get('model'), 'servers', server); - this.send('openModal', 'apis/server_form'); - }, - - deleteServer: function(server) { - this.deleteChildRecord('servers', server, 'Are you sure you want to remove this server?'); - }, - - addUrlMatch: function() { - this.get('controllers.apis_url_match_form').add(this.get('model'), 'urlMatches'); - this.send('openModal', 'apis/url_match_form'); - }, - - editUrlMatch: function(urlMatch) { - this.get('controllers.apis_url_match_form').edit(this.get('model'), 'urlMatches', urlMatch); - this.send('openModal', 'apis/url_match_form'); - }, - - deleteUrlMatch: function(urlMatch) { - this.deleteChildRecord('urlMatches', urlMatch, 'Are you sure you want to remove this URL prefix?'); - }, - - addSubSettings: function() { - this.get('controllers.apis_sub_settings_form').add(this.get('model'), 'subSettings'); - this.send('openModal', 'apis/sub_settings_form'); - }, - - editSubSettings: function(subSettings) { - this.get('controllers.apis_sub_settings_form').edit(this.get('model'), 'subSettings', subSettings); - this.send('openModal', 'apis/sub_settings_form'); - }, - - deleteSubSettings: function(subSettings) { - this.deleteChildRecord('subSettings', subSettings, 'Are you sure you want to remove this URL setting?'); - }, - - addRewrite: function() { - this.get('controllers.apis_rewrite_form').add(this.get('model'), 'rewrites'); - this.send('openModal', 'apis/rewrite_form'); - }, - - editRewrite: function(rewrite) { - this.get('controllers.apis_rewrite_form').edit(this.get('model'), 'rewrites', rewrite); - this.send('openModal', 'apis/rewrite_form'); - }, - - deleteRewrite: function(rewrite) { - this.deleteChildRecord('rewrites', rewrite, 'Are you sure you want to remove this rewrite?'); - }, - }, - - deleteChildRecord: function(collectionName, record, message) { - var collection = this.get('model').get(collectionName); - bootbox.confirm(message, function(result) { - if(result) { - collection.removeObject(record); - } - }); - }, -}); - -Admin.ApisEditController = Admin.ApisFormController.extend(); -Admin.ApisNewController = Admin.ApisFormController.extend(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_index_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_index_controller.js deleted file mode 100644 index 7eb9ba14c..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_index_controller.js +++ /dev/null @@ -1,9 +0,0 @@ -Admin.ApisIndexController = Ember.ArrayController.extend({ - reorderActive: false, - - actions: { - toggleReorderApis: function() { - this.set('reorderActive', !this.get('reorderActive')); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_server_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_server_controller.js deleted file mode 100644 index c14cb3273..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/apis_server_controller.js +++ /dev/null @@ -1,3 +0,0 @@ -Admin.ApisServerController = Ember.Controller.extend({ - title: 'HELLO' -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/application.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/application.js deleted file mode 100644 index 0bf4b56fc..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/application.js +++ /dev/null @@ -1,3 +0,0 @@ -Admin.ApplicationController = Ember.ObjectController.extend({ - isLoading: null, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/config/publish_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/config/publish_controller.js deleted file mode 100644 index 58dcacc16..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/config/publish_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -Admin.ConfigPublishController = Ember.Controller.extend({ - hasChanges: function() { - var newApis = this.get('model.config.apis.new'); - var modifiedApis = this.get('model.config.apis.modified'); - var deletedApis = this.get('model.config.apis.deleted'); - var newWebsiteBackends = this.get('model.config.website_backends.new'); - var modifiedWebsiteBackends = this.get('model.config.website_backends.modified'); - var deletedWebsiteBackends = this.get('model.config.website_backends.deleted'); - - if(newApis.length > 0 || modifiedApis.length > 0 || deletedApis.length > 0 || newWebsiteBackends.length > 0 || modifiedWebsiteBackends.length > 0 || deletedWebsiteBackends.length > 0) { - return true; - } else { - return false; - } - }.property('model.config.apis.new.@each', 'model.config.apis.modified.@each', 'model.config.apis.deleted.@each', 'model.config.website_backends.new.@each', 'model.config.website_backends.modified.@each', 'model.config.website_backends.deleted.@each'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/modal_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/modal_controller.js deleted file mode 100644 index b90fb80c3..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/modal_controller.js +++ /dev/null @@ -1,3 +0,0 @@ -Admin.ModalController = Ember.Controller.extend({ - title: 'Example Modal Title' -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/base_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/base_controller.js deleted file mode 100644 index d41068d14..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/base_controller.js +++ /dev/null @@ -1,22 +0,0 @@ -Admin.StatsBaseController = Ember.ObjectController.extend({ - needs: ['application'], - - query: null, - - actions: { - submit: function() { - var query = this.get('query'); - query.beginPropertyChanges(); - - if($('#filter_type_advanced').css('display') === 'none') { - query.set('params.search', ''); - query.set('params.query', JSON.stringify($('#query_builder').queryBuilder('getRules'))); - } else { - query.set('params.query', ''); - query.set('params.search', $('#filter_form input[name=search]').val()); - } - - query.endPropertyChanges(); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/drilldown_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/drilldown_controller.js deleted file mode 100644 index 9de8fdc72..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/drilldown_controller.js +++ /dev/null @@ -1,34 +0,0 @@ -Admin.StatsDrilldownController = Admin.StatsBaseController.extend({ - breadcrumbs: function() { - var crumbs = []; - - var data = this.get('model.breadcrumbs'); - for(var i = 0; i < data.length; i++) { - var crumb = { name: data[i].crumb }; - - if(i < data.length -1) { - var params = _.clone(this.get('query.params')); - params.prefix = data[i].prefix; - crumb.linkQuery = $.param(params); - } - - crumbs.push(crumb); - } - - if(crumbs.length <= 1) { - crumbs = []; - } - - return crumbs; - }.property('model.breadcrumbs'), - - downloadUrl: function() { - return '/api-umbrella/v1/analytics/drilldown.csv?' + $.param(this.get('query.params')) + '&api_key=' + webAdminAjaxApiKey; - }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.prefix', 'query.params.beta_analytics'), -}); - -Admin.StatsDrilldownDefaultController = Admin.StatsDrilldownController.extend({ - renderTemplate: function() { - this.render('stats/drilldown'); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/logs_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/logs_controller.js deleted file mode 100644 index 1c18c8083..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/logs_controller.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.StatsLogsController = Admin.StatsBaseController.extend({ - downloadUrl: function() { - return '/admin/stats/logs.csv?' + $.param(this.get('query.params')); - }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at', 'query.params.beta_analytics'), -}); - -Admin.StatsLogsDefaultController = Admin.StatsLogsController.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/map_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/map_controller.js deleted file mode 100644 index e408383ce..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/map_controller.js +++ /dev/null @@ -1,30 +0,0 @@ -Admin.StatsMapController = Admin.StatsBaseController.extend({ - breadcrumbs: function() { - var crumbs = []; - - var data = this.get('model.map_breadcrumbs'); - for(var i = 0; i < data.length; i++) { - var crumb = { name: data[i].name }; - - if(i < data.length -1) { - var params = _.clone(this.get('query.params')); - params.region = data[i].region; - crumb.linkQuery = $.param(params); - } - - crumbs.push(crumb); - } - - return crumbs; - }.property('model.breadcrumb'), - - downloadUrl: function() { - return '/admin/stats/map.csv?' + $.param(this.get('query.params')); - }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.beta_analytics'), -}); - -Admin.StatsMapDefaultController = Admin.StatsMapController.extend({ - renderTemplate: function() { - this.render('stats/users'); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/users_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/users_controller.js deleted file mode 100644 index 1fa327679..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/stats/users_controller.js +++ /dev/null @@ -1,11 +0,0 @@ -Admin.StatsUsersController = Admin.StatsBaseController.extend({ - downloadUrl: function() { - return '/admin/stats/users.csv?' + $.param(this.get('query.params')); - }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.beta_analytics'), -}); - -Admin.StatsUsersDefaultController = Admin.StatsUsersController.extend({ - renderTemplate: function() { - this.render('stats/users'); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/website_backends/form_controller.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/website_backends/form_controller.js deleted file mode 100644 index bf758a445..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/controllers/website_backends/form_controller.js +++ /dev/null @@ -1,41 +0,0 @@ -Admin.WebsiteBackendsFormController = Ember.ObjectController.extend(Admin.Save, { - backendProtocolOptions: [ - { id: 'http', name: 'http' }, - { id: 'https', name: 'https' }, - ], - - changeDefaultPort: function() { - var protocol = this.get('model.backendProtocol'); - var port = parseInt(this.get('model.serverPort'), 10); - if(protocol === 'https') { - if(!port || port === 80) { - this.set('model.serverPort', 443); - } - } else { - if(!port || port === 443) { - this.set('model.serverPort', 80); - } - } - }.observes('model.backendProtocol'), - - actions: { - submit: function() { - this.save({ - transitionToRoute: 'website_backends', - message: 'Successfully saved the "' + _.escape(this.get('model.frontendHost')) + '" website backend
Note: Your changes are not yet live. Publish Changes to send your updates live.', - }); - }, - - delete: function() { - bootbox.confirm('Are you sure you want to delete this website backend?', _.bind(function(result) { - if(result) { - this.get('model').deleteRecord(); - this.transitionToRoute('website_backends'); - } - }, this)); - }, - }, -}); - -Admin.WebsiteBackendsEditController = Admin.WebsiteBackendsFormController.extend(); -Admin.WebsiteBackendsNewController = Admin.WebsiteBackendsFormController.extend(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/de.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/de.js.erb deleted file mode 100644 index b77c25540..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/de.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -//= depend_on "de.yml" -//= require polyglot -<%= JsLocaleHelper.output_locale(:de) %> diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/en.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/en.js.erb deleted file mode 100644 index 5968984c3..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/en.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -//= depend_on "en.yml" -//= require polyglot -<%= JsLocaleHelper.output_locale(:en) %> diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/es-419.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/es-419.js.erb deleted file mode 100644 index c47343eff..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/es-419.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -//= depend_on "es-419.yml" -//= require polyglot -<%= JsLocaleHelper.output_locale(:"es-419") %> diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/fi.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/fi.js.erb deleted file mode 100644 index 5c5423b0f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/fi.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -//= depend_on "fi.yml" -//= require polyglot -<%= JsLocaleHelper.output_locale(:fi) %> diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/fr.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/fr.js.erb deleted file mode 100644 index 0254c0fd9..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/fr.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -//= depend_on "fr.yml" -//= require polyglot -<%= JsLocaleHelper.output_locale(:fr) %> diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/it.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/it.js.erb deleted file mode 100644 index c4cb74684..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/it.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -//= depend_on "it.yml" -//= require polyglot -<%= JsLocaleHelper.output_locale(:it) %> diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/ru.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/ru.js.erb deleted file mode 100644 index 142719c62..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/locales/ru.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -//= depend_on "ru.yml" -//= require polyglot -<%= JsLocaleHelper.output_locale(:ru) %> diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin.js deleted file mode 100644 index 2b509d964..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin.js +++ /dev/null @@ -1,24 +0,0 @@ -Admin.Admin = Ember.Model.extend({ - id: Ember.attr(), - username: Ember.attr(), - email: Ember.attr(), - name: Ember.attr(), - superuser: Ember.attr(), - groupIds: Ember.attr(), - signInCount: Ember.attr(), - lastSignInAt: Ember.attr(), - lastSignInIp: Ember.attr(), - lastSignInProvider: Ember.attr(), - authenticationToken: Ember.attr(), - createdAt: Ember.attr(), - updatedAt: Ember.attr(), - creator: Ember.attr(), - updater: Ember.attr(), -}); - -Admin.Admin.url = '/api-umbrella/v1/admins'; -Admin.Admin.rootKey = 'admin'; -Admin.Admin.collectionKey = 'data'; -Admin.Admin.primaryKey = 'id'; -Admin.Admin.camelizeKeys = true; -Admin.Admin.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin_group.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin_group.js deleted file mode 100644 index d71c7522c..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin_group.js +++ /dev/null @@ -1,24 +0,0 @@ -Admin.AdminGroup = Ember.Model.extend(Ember.Validations.Mixin, { - id: Ember.attr(), - name: Ember.attr(), - apiScopeIds: Ember.attr(), - permissionIds: Ember.attr(), - admins: Ember.attr(), - createdAt: Ember.attr(), - updatedAt: Ember.attr(), - creator: Ember.attr(), - updater: Ember.attr(), - - validations: { - name: { - presence: true, - }, - }, -}); - -Admin.AdminGroup.url = '/api-umbrella/v1/admin_groups'; -Admin.AdminGroup.rootKey = 'admin_group'; -Admin.AdminGroup.collectionKey = 'data'; -Admin.AdminGroup.primaryKey = 'id'; -Admin.AdminGroup.camelizeKeys = true; -Admin.AdminGroup.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin_permission.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin_permission.js deleted file mode 100644 index 1e07e567a..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/admin_permission.js +++ /dev/null @@ -1,11 +0,0 @@ -Admin.AdminPermission = Ember.Model.extend({ - id: Ember.attr(), - name: Ember.attr() -}); - -Admin.AdminPermission.url = '/api-umbrella/v1/admin_permissions'; -Admin.AdminPermission.rootKey = 'admin_permission'; -Admin.AdminPermission.collectionKey = 'admin_permissions'; -Admin.AdminPermission.primaryKey = 'id'; -Admin.AdminPermission.camelizeKeys = true; -Admin.AdminPermission.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api.js deleted file mode 100644 index 2257d294b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api.js +++ /dev/null @@ -1,82 +0,0 @@ -Admin.Api = Ember.Model.extend(Ember.Validations.Mixin, { - name: Ember.attr(), - sortOrder: Ember.attr(Number), - backendProtocol: Ember.attr(), - frontendHost: Ember.attr(), - backendHost: Ember.attr(), - balanceAlgorithm: Ember.attr(), - createdAt: Ember.attr(), - updatedAt: Ember.attr(), - creator: Ember.attr(), - updater: Ember.attr(), - - servers: Ember.hasMany('Admin.ApiServer', { key: 'servers', embedded: true }), - urlMatches: Ember.hasMany('Admin.ApiUrlMatch', { key: 'url_matches', embedded: true }), - settings: Ember.belongsTo('Admin.ApiSettings', { key: 'settings', embedded: true }), - subSettings: Ember.hasMany('Admin.ApiSubSettings', { key: 'sub_settings', embedded: true }), - rewrites: Ember.hasMany('Admin.ApiRewrite', { key: 'rewrites', embedded: true }), - - validations: { - name: { - presence: true, - }, - frontendHost: { - presence: true, - format: { - with: CommonValidations.host_format_with_wildcard, - message: polyglot.t('errors.messages.invalid_host_format'), - }, - }, - backendHost: { - presence: { - unless: function(object) { - return (object.get('frontendHost') && object.get('frontendHost')[0] === '*'); - }, - }, - format: { - with: CommonValidations.host_format_with_wildcard, - message: polyglot.t('errors.messages.invalid_host_format'), - if: function(object) { - return !!object.get('backendHost'); - }, - }, - }, - }, - - init: function() { - this._super(); - - // Set defaults for new records. - this.setDefaults(); - - // For existing records, we need to set the defaults after loading. - this.on('didLoad', this, this.setDefaults); - }, - - setDefaults: function() { - if(!this.get('settings')) { - this.set('settings', Admin.ApiSettings.create()); - } - }, - - exampleIncomingUrlRoot: function() { - return 'http://' + (this.get('frontendHost') || ''); - }.property('frontendHost'), - - exampleOutgoingUrlRoot: function() { - return 'http://' + (this.get('backendHost') || this.get('frontendHost') || ''); - }.property('backendHost'), - - didSaveRecord: function() { - // Clear the cached roles on save, so the list of available roles is always - // correct for subsequent form renderings in this current session. - Admin.ApiUserRole.clearCache(); - }, -}); - -Admin.Api.url = '/api-umbrella/v1/apis'; -Admin.Api.rootKey = 'api'; -Admin.Api.collectionKey = 'data'; -Admin.Api.primaryKey = 'id'; -Admin.Api.camelizeKeys = true; -Admin.Api.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/rate_limit.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/rate_limit.js deleted file mode 100644 index 31761f698..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/rate_limit.js +++ /dev/null @@ -1,67 +0,0 @@ -Admin.ApiRateLimit = Ember.Model.extend({ - id: Ember.attr(), - duration: Ember.attr(Number), - limitBy: Ember.attr(), - limit: Ember.attr(), - responseHeaders: Ember.attr(), - - //durationUnits: Ember.attr(), - //durationInUnits: Ember.attr(Number), - - init: function() { - this._super(); - - // Set defaults for new records. - this.setDefaults(); - - // For existing records, we need to set the defaults after loading. - this.on('didLoad', this, this.setDefaults); - }, - - setDefaults: function() { - var duration = this.get('duration'); - if(duration) { - var days = duration / 86400000; - var hours = duration / 3600000; - var minutes = duration / 60000; - var seconds = duration / 1000; - - if(days % 1 === 0) { - this.set('durationInUnits', days); - this.set('durationUnits', 'days'); - } else if(hours % 1 === 0) { - this.set('durationInUnits', hours); - this.set('durationUnits', 'hours'); - } else if(minutes % 1 === 0) { - this.set('durationInUnits', minutes); - this.set('durationUnits', 'minutes'); - } else { - this.set('durationInUnits', seconds); - this.set('durationUnits', 'seconds'); - } - } - }, - - durationFromUnits: function() { - if(this.get('durationInUnits') && this.get('durationUnits')) { - var inUnits = parseInt(this.get('durationInUnits'), 10); - var units = this.get('durationUnits'); - return moment.duration(inUnits, units).asMilliseconds(); - } else { - return this.get('duration'); - } - }.property('durationInUnits', 'durationUnits'), - - settingsId: function() { - return this.get('parent.id'); - }.property(), - - toJSON: function() { - var json = this._super(); - json.duration = this.get('durationFromUnits'); - return json; - }, -}); - -Admin.ApiRateLimit.primaryKey = 'id'; -Admin.ApiRateLimit.camelizeKeys = true; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/rewrite.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/rewrite.js deleted file mode 100644 index 1d2095ce5..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/rewrite.js +++ /dev/null @@ -1,11 +0,0 @@ -Admin.ApiRewrite = Ember.Model.extend({ - id: Ember.attr(), - sortOrder: Ember.attr(Number), - matcherType: Ember.attr(), - httpMethod: Ember.attr(), - frontendMatcher: Ember.attr(), - backendReplacement: Ember.attr(), -}); - -Admin.ApiRewrite.primaryKey = 'id'; -Admin.ApiRewrite.camelizeKeys = true; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/server.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/server.js deleted file mode 100644 index 7a35dd3c4..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/server.js +++ /dev/null @@ -1,26 +0,0 @@ -Admin.ApiServer = Ember.Model.extend(Ember.Validations.Mixin, { - id: Ember.attr(), - host: Ember.attr(), - port: Ember.attr(Number), - - validations: { - host: { - presence: true, - format: { - with: CommonValidations.host_format, - message: polyglot.t('errors.messages.invalid_host_format'), - }, - }, - port: { - presence: true, - numericality: true, - }, - }, - - hostWithPort: function() { - return _.compact([this.get('host'), this.get('port')]).join(':'); - }.property('host', 'port'), -}); - -Admin.ApiServer.primaryKey = 'id'; -Admin.ApiServer.camelizeKeys = true; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/settings.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/settings.js deleted file mode 100644 index eb019fedd..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/settings.js +++ /dev/null @@ -1,108 +0,0 @@ -Admin.ApiSettings = Ember.Model.extend({ - id: Ember.attr(), - appendQueryString: Ember.attr(), - headersString: Ember.attr(), - httpBasicAuth: Ember.attr(), - requireHttps: Ember.attr(), - disableApiKey: Ember.attr(), - apiKeyVerificationLevel: Ember.attr(), - requiredRoles: Ember.attr(), - requiredRolesOverride: Ember.attr(), - allowedIps: Ember.attr(), - allowedReferers: Ember.attr(), - rateLimitMode: Ember.attr(), - anonymousRateLimitBehavior: Ember.attr(), - authenticatedRateLimitBehavior: Ember.attr(), - passApiKeyHeader: Ember.attr(), - passApiKeyQueryParam: Ember.attr(), - defaultResponseHeadersString: Ember.attr(), - overrideResponseHeadersString: Ember.attr(), - errorTemplates: Ember.attr(), - errorDataYamlStrings: Ember.attr(), - - rateLimits: Ember.hasMany('Admin.ApiRateLimit', { key: 'rate_limits', embedded: true }), - - init: function() { - this._super(); - - // Set defaults for new records. - this.setDefaults(); - - // For existing records, we need to set the defaults after loading. - this.on('didLoad', this, this.setDefaults); - }, - - setDefaults: function() { - if(this.get('rateLimitMode') === undefined) { - this.set('rateLimitMode', null); - } - - // Make sure at least an empty object exists so the form builder can dive - // into this section even when there's no pre-existing data. - if(!this.get('errorTemplates')) { - this.set('errorTemplates', Ember.Object.create({})); - } - - if(!this.get('errorDataYamlStrings')) { - this.set('errorDataYamlStrings', Ember.Object.create({})); - } - }, - - requiredRolesString: function(key, value) { - // Setter - if(arguments.length > 1) { - var roles = _.compact(value.split(',')); - if(roles.length === 0) { roles = null; } - this.set('requiredRoles', roles); - } - - // Getter - var rolesString = ''; - if(this.get('requiredRoles')) { - rolesString = this.get('requiredRoles').join(','); - } - - return rolesString; - }.property('requiredRoles'), - - allowedIpsString: function(key, value) { - // Setter - if(arguments.length > 1) { - var ips = _.compact(value.split(/[\r\n]+/)); - if(ips.length === 0) { ips = null; } - this.set('allowedIps', ips); - } - - // Getter - var allowedIpsString = ''; - if(this.get('allowedIps')) { - allowedIpsString = this.get('allowedIps').join('\n'); - } - - return allowedIpsString; - }.property('allowedIps'), - - allowedReferersString: function(key, value) { - // Setter - if(arguments.length > 1) { - var referers = _.compact(value.split(/[\r\n]+/)); - if(referers.length === 0) { referers = null; } - this.set('allowedReferers', referers); - } - - // Getter - var allowedReferersString = ''; - if(this.get('allowedReferers')) { - allowedReferersString = this.get('allowedReferers').join('\n'); - } - - return allowedReferersString; - }.property('allowedReferers'), - - isRateLimitModeCustom: function() { - return (this.get('rateLimitMode') === 'custom'); - }.property('rateLimitMode'), -}); - -Admin.ApiSettings.primaryKey = 'id'; -Admin.ApiSettings.camelizeKeys = true; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/sub_settings.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/sub_settings.js deleted file mode 100644 index 90bb037de..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/sub_settings.js +++ /dev/null @@ -1,27 +0,0 @@ -Admin.ApiSubSettings = Ember.Model.extend({ - id: Ember.attr(), - sortOrder: Ember.attr(Number), - httpMethod: Ember.attr(), - regex: Ember.attr(), - - settings: Ember.belongsTo('Admin.ApiSettings', { key: 'settings', embedded: true }), - - init: function() { - this._super(); - - // Set defaults for new records. - this.setDefaults(); - - // For existing records, we need to set the defaults after loading. - this.on('didLoad', this, this.setDefaults); - }, - - setDefaults: function() { - if(!this.get('settings')) { - this.set('settings', Admin.ApiSettings.create()); - } - }, -}); - -Admin.ApiSubSettings.primaryKey = 'id'; -Admin.ApiSubSettings.camelizeKeys = true; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/url_match.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/url_match.js deleted file mode 100644 index e5b86d088..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api/url_match.js +++ /dev/null @@ -1,30 +0,0 @@ -Admin.ApiUrlMatch = Ember.Model.extend(Ember.Validations.Mixin,{ - id: Ember.attr(), - sortOrder: Ember.attr(Number), - frontendPrefix: Ember.attr(), - backendPrefix: Ember.attr(), - - validations: { - frontendPrefix: { - presence: true, - format: { - with: CommonValidations.url_prefix_format, - message: polyglot.t('errors.messages.invalid_url_prefix_format'), - }, - }, - backendPrefix: { - presence: true, - format: { - with: CommonValidations.url_prefix_format, - message: polyglot.t('errors.messages.invalid_url_prefix_format'), - }, - }, - }, - - backendPrefixWithDefault: function() { - return this.get('backendPrefix') || this.get('frontendPrefix'); - }.property('backendPrefix', 'frontendPrefix'), -}); - -Admin.ApiUrlMatch.primaryKey = 'id'; -Admin.ApiUrlMatch.camelizeKeys = true; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_scope.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_scope.js deleted file mode 100644 index e1895cb8b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_scope.js +++ /dev/null @@ -1,41 +0,0 @@ -Admin.ApiScope = Ember.Model.extend(Ember.Validations.Mixin, { - id: Ember.attr(), - name: Ember.attr(), - host: Ember.attr(), - pathPrefix: Ember.attr(), - createdAt: Ember.attr(), - updatedAt: Ember.attr(), - creator: Ember.attr(), - updater: Ember.attr(), - - validations: { - name: { - presence: true, - }, - host: { - presence: true, - format: { - with: CommonValidations.host_format_with_wildcard, - message: polyglot.t('errors.messages.invalid_host_format'), - }, - }, - pathPrefix: { - presence: true, - format: { - with: CommonValidations.url_prefix_format, - message: polyglot.t('errors.messages.invalid_url_prefix_format'), - }, - }, - }, - - displayName: function() { - return this.get('name') + ' - ' + this.get('host') + this.get('pathPrefix'); - }.property('name', 'host', 'pathPrefix') -}); - -Admin.ApiScope.url = '/api-umbrella/v1/api_scopes'; -Admin.ApiScope.rootKey = 'api_scope'; -Admin.ApiScope.collectionKey = 'data'; -Admin.ApiScope.primaryKey = 'id'; -Admin.ApiScope.camelizeKeys = true; -Admin.ApiScope.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_user.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_user.js deleted file mode 100644 index 2372895d8..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_user.js +++ /dev/null @@ -1,97 +0,0 @@ -Admin.ApiUser = Ember.Model.extend(Ember.Validations.Mixin, { - id: Ember.attr(), - apiKey: Ember.attr(), - apiKeyHidesAt: Ember.attr(), - apiKeyPreview: Ember.attr(), - firstName: Ember.attr(), - lastName: Ember.attr(), - email: Ember.attr(), - emailVerified: Ember.attr(), - website: Ember.attr(), - useDescription: Ember.attr(), - registrationSource: Ember.attr(), - termsAndConditions: Ember.attr(), - sendWelcomeEmail: Ember.attr(), - throttleByIp: Ember.attr(), - roles: Ember.attr(), - enabled: Ember.attr(), - createdAt: Ember.attr(), - updatedAt: Ember.attr(), - creator: Ember.attr(), - updater: Ember.attr(), - registrationIp: Ember.attr(), - registrationUserAgent: Ember.attr(), - registrationReferer: Ember.attr(), - registrationOrigin: Ember.attr(), - - settings: Ember.belongsTo('Admin.ApiSettings', { key: 'settings', embedded: true }), - - validations: { - firstName: { - presence: true, - }, - lastName: { - presence: true, - }, - email: { - presence: true, - }, - }, - - init: function() { - this._super(); - - // Set defaults for new records. - this.setDefaults(); - - // For existing records, we need to set the defaults after loading. - this.on('didLoad', this, this.setDefaults); - }, - - setDefaults: function() { - if(this.get('throttleByIp') === undefined) { - this.set('throttleByIp', false); - } - - if(this.get('enabled') === undefined) { - this.set('enabled', true); - } - - if(!this.get('settings')) { - this.set('settings', Admin.ApiSettings.create()); - } - - if(!this.get('registrationSource') && this.get('isNew')) { - this.set('registrationSource', 'web_admin'); - } - }, - - rolesString: function(key, value) { - // Setter - if(arguments.length > 1) { - var roles = value.split(','); - this.set('roles', roles); - } - - // Getter - var rolesString = ''; - if(this.get('roles')) { - rolesString = this.get('roles').join(','); - } - - return rolesString; - }.property('roles'), - - didSaveRecord: function() { - // Clear the cached roles on save, so the list of available roles is always - // correct for subsequent form renderings in this current session. - Admin.ApiUserRole.clearCache(); - }, -}); - -Admin.ApiUser.url = '/api-umbrella/v1/users'; -Admin.ApiUser.rootKey = 'user'; -Admin.ApiUser.collectionKey = 'data'; -Admin.ApiUser.primaryKey = 'id'; -Admin.ApiUser.camelizeKeys = true; -Admin.ApiUser.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_user_role.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_user_role.js deleted file mode 100644 index 72efe7707..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/api_user_role.js +++ /dev/null @@ -1,10 +0,0 @@ -Admin.ApiUserRole = Ember.Model.extend({ - id: Ember.attr(), -}); - -Admin.ApiUserRole.url = '/api-umbrella/v1/user_roles'; -Admin.ApiUserRole.rootKey = 'user_roles'; -Admin.ApiUserRole.collectionKey = 'user_roles'; -Admin.ApiUserRole.primaryKey = 'id'; -Admin.ApiUserRole.camelizeKeys = true; -Admin.ApiUserRole.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/drilldown.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/drilldown.js deleted file mode 100644 index 53f283e6c..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/drilldown.js +++ /dev/null @@ -1,21 +0,0 @@ -Admin.StatsDrilldown = Ember.Object.extend(Ember.Evented, { - results: null, -}); - -Admin.StatsDrilldown.reopenClass({ - find: function(params) { - var promise = Ember.Deferred.create(); - - $.ajax({ - url: '/api-umbrella/v1/analytics/drilldown.json', - data: params, - }).done(function(data) { - var map = Admin.StatsDrilldown.create(data); - promise.resolve(map); - }).fail(function() { - promise.reject(); - }); - - return promise; - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/logs.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/logs.js deleted file mode 100644 index 0663d1474..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/logs.js +++ /dev/null @@ -1,24 +0,0 @@ -Admin.StatsLogs = Ember.Object.extend(Ember.Evented, { - hits_over_time: null, - stats: null, - facets: null, - logs: null, -}); - -Admin.StatsLogs.reopenClass({ - find: function(params) { - var promise = Ember.Deferred.create(); - - $.ajax({ - url: '/admin/stats/search.json', - data: params, - }).done(function(data) { - var stats = Admin.StatsLogs.create(data); - promise.resolve(stats); - }).fail(function() { - promise.reject(); - }); - - return promise; - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/map.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/map.js deleted file mode 100644 index cbfae9485..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/stats/map.js +++ /dev/null @@ -1,22 +0,0 @@ -Admin.StatsMap = Ember.Object.extend(Ember.Evented, { - regions: null, - map_regions: null, -}); - -Admin.StatsMap.reopenClass({ - find: function(params) { - var promise = Ember.Deferred.create(); - - $.ajax({ - url: '/admin/stats/map.json', - data: params, - }).done(function(data) { - var map = Admin.StatsMap.create(data); - promise.resolve(map); - }).fail(function() { - promise.reject(); - }); - - return promise; - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/website_backend.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/models/website_backend.js deleted file mode 100644 index 5f792d711..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/models/website_backend.js +++ /dev/null @@ -1,38 +0,0 @@ -Admin.WebsiteBackend = Ember.Model.extend(Ember.Validations.Mixin, { - id: Ember.attr(), - frontendHost: Ember.attr(), - backendProtocol: Ember.attr(), - serverHost: Ember.attr(), - serverPort: Ember.attr(Number), - - validations: { - frontendHost: { - presence: true, - format: { - with: CommonValidations.host_format_with_wildcard, - message: polyglot.t('errors.messages.invalid_host_format'), - }, - }, - backendProtocol: { - presence: true, - }, - serverHost: { - presence: true, - format: { - with: CommonValidations.host_format_with_wildcard, - message: polyglot.t('errors.messages.invalid_host_format'), - }, - }, - serverPort: { - presence: true, - numericality: true, - }, - }, -}); - -Admin.WebsiteBackend.url = '/api-umbrella/v1/website_backends'; -Admin.WebsiteBackend.rootKey = 'website_backend'; -Admin.WebsiteBackend.collectionKey = 'data'; -Admin.WebsiteBackend.primaryKey = 'id'; -Admin.WebsiteBackend.camelizeKeys = true; -Admin.WebsiteBackend.adapter = Admin.APIUmbrellaRESTAdapter.create(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/router.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/router.js deleted file mode 100644 index 4c81d5ffc..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/router.js +++ /dev/null @@ -1,49 +0,0 @@ -Admin.Router.map(function() { - this.resource('apis', { path: '/apis' }, function() { - this.route('new'); - this.route('edit', { path: '/:apiId/edit' }); - }); - - this.resource('api_users', { path: '/api_users' }, function() { - this.route('new'); - this.route('edit', { path: '/:apiUserId/edit' }); - }); - - this.resource('admins', { path: '/admins' }, function() { - this.route('new'); - this.route('edit', { path: '/:adminId/edit' }); - }); - - this.resource('api_scopes', { path: '/api_scopes' }, function() { - this.route('new'); - this.route('edit', { path: '/:apiScopeId/edit' }); - }); - - this.resource('admin_groups', { path: '/admin_groups' }, function() { - this.route('new'); - this.route('edit', { path: '/:adminGroupId/edit' }); - }); - - this.resource('config', { path: '/config' }, function() { - this.route('publish'); - }); - - this.resource('stats', { path: '/stats' }, function() { - this.route('drilldown', { path: '/drilldown/*query' }); - this.route('drilldownDefault', { path: '/drilldown' }); - - this.route('logs', { path: '/logs/*query' }); - this.route('logsDefault', { path: '/logs' }); - - this.route('users', { path: '/users/*query' }); - this.route('usersDefault', { path: '/users' }); - - this.route('map', { path: '/map/*query' }); - this.route('mapDefault', { path: '/map' }); - }); - - this.resource('website_backends', { path: '/website_backends' }, function() { - this.route('new'); - this.route('edit', { path: '/:websiteBackendId/edit' }); - }); -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/base_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/base_route.js deleted file mode 100644 index 4fd32f526..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/base_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.AdminGroupsBaseRoute = Ember.Route.extend({ - setupController: function(controller, model) { - controller.set('model', model); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-users').addClass('active'); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/edit_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/edit_route.js deleted file mode 100644 index 30e77806b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/edit_route.js +++ /dev/null @@ -1,9 +0,0 @@ -Admin.AdminGroupsEditRoute = Admin.AdminGroupsBaseRoute.extend({ - model: function(params) { - // Clear the record cache, so this is always fetched from the server (to - // account for two users simultaneously editing the same record). - Admin.AdminGroup.clearCache(); - - return Admin.AdminGroup.find(params.adminGroupId); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/index_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/index_route.js deleted file mode 100644 index db28c3a36..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/index_route.js +++ /dev/null @@ -1,2 +0,0 @@ -Admin.AdminGroupsIndexRoute = Admin.AdminGroupsBaseRoute.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/new_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/new_route.js deleted file mode 100644 index 622adf7a8..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admin_groups/new_route.js +++ /dev/null @@ -1,6 +0,0 @@ -Admin.AdminGroupsNewRoute = Admin.AdminGroupsBaseRoute.extend({ - model: function() { - return Admin.AdminGroup.create(); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/base_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/base_route.js deleted file mode 100644 index e41accc57..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/base_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.AdminsBaseRoute = Ember.Route.extend({ - setupController: function(controller, model) { - controller.set('model', model); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-users').addClass('active'); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/edit_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/edit_route.js deleted file mode 100644 index c2e8d0b60..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/edit_route.js +++ /dev/null @@ -1,9 +0,0 @@ -Admin.AdminsEditRoute = Admin.AdminsBaseRoute.extend({ - model: function(params) { - // Clear the record cache, so this is always fetched from the server (to - // account for two users simultaneously editing the same record). - Admin.Admin.clearCache(); - - return Admin.Admin.find(params.adminId); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/index_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/index_route.js deleted file mode 100644 index 5b46d86ed..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/index_route.js +++ /dev/null @@ -1,2 +0,0 @@ -Admin.AdminsIndexRoute = Admin.AdminsBaseRoute.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/new_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/new_route.js deleted file mode 100644 index b506a4219..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/admins/new_route.js +++ /dev/null @@ -1,6 +0,0 @@ -Admin.AdminsNewRoute = Admin.AdminsBaseRoute.extend({ - model: function() { - return Admin.Admin.create(); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/base_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/base_route.js deleted file mode 100644 index 5185b09cd..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/base_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.ApiScopesBaseRoute = Ember.Route.extend({ - setupController: function(controller, model) { - controller.set('model', model); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-users').addClass('active'); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/edit_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/edit_route.js deleted file mode 100644 index c2ac168e5..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/edit_route.js +++ /dev/null @@ -1,9 +0,0 @@ -Admin.ApiScopesEditRoute = Admin.ApiScopesBaseRoute.extend({ - model: function(params) { - // Clear the record cache, so this is always fetched from the server (to - // account for two users simultaneously editing the same record). - Admin.ApiScope.clearCache(); - - return Admin.ApiScope.find(params.apiScopeId); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/index_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/index_route.js deleted file mode 100644 index 5bcb4a732..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/index_route.js +++ /dev/null @@ -1,2 +0,0 @@ -Admin.ApiScopesIndexRoute = Admin.ApiScopesBaseRoute.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/new_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/new_route.js deleted file mode 100644 index 4bca8b857..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_scopes/new_route.js +++ /dev/null @@ -1,6 +0,0 @@ -Admin.ApiScopesNewRoute = Admin.ApiScopesBaseRoute.extend({ - model: function() { - return Admin.ApiScope.create(); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/base_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/base_route.js deleted file mode 100644 index e88ba48ea..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/base_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.ApiUsersBaseRoute = Ember.Route.extend({ - setupController: function(controller, model) { - controller.set('model', model); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-users').addClass('active'); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/edit_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/edit_route.js deleted file mode 100644 index 2ac81be8b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/edit_route.js +++ /dev/null @@ -1,9 +0,0 @@ -Admin.ApiUsersEditRoute = Admin.ApiUsersBaseRoute.extend({ - model: function(params) { - // Clear the record cache, so this is always fetched from the server (to - // account for two users simultaneously editing the same record). - Admin.ApiUser.clearCache(); - - return Admin.ApiUser.find(params.apiUserId); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/index_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/index_route.js deleted file mode 100644 index 32c93bad3..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/index_route.js +++ /dev/null @@ -1,2 +0,0 @@ -Admin.ApiUsersIndexRoute = Admin.ApiUsersBaseRoute.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/new_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/new_route.js deleted file mode 100644 index d6d37b429..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/api_users/new_route.js +++ /dev/null @@ -1,6 +0,0 @@ -Admin.ApiUsersNewRoute = Admin.ApiUsersBaseRoute.extend({ - model: function() { - return Admin.ApiUser.create(); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/base_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/base_route.js deleted file mode 100644 index 3c0fb3fad..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/base_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.ApisBaseRoute = Ember.Route.extend({ - setupController: function(controller, model) { - controller.set('model', model); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-config').addClass('active'); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/edit_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/edit_route.js deleted file mode 100644 index aaf72486c..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/edit_route.js +++ /dev/null @@ -1,10 +0,0 @@ -Admin.ApisEditRoute = Admin.ApisBaseRoute.extend({ - model: function(params) { - // Clear the record cache, so this is always fetched from the server (to - // account for two users simultaneously editing the same record). - Admin.Api.clearCache(); - - return Admin.Api.find(params.apiId); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/index_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/index_route.js deleted file mode 100644 index 1bd5bab00..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/index_route.js +++ /dev/null @@ -1,2 +0,0 @@ -Admin.ApisIndexRoute = Admin.ApisBaseRoute.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/new_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/new_route.js deleted file mode 100644 index c6c3678b4..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/apis/new_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.ApisNewRoute = Admin.ApisBaseRoute.extend({ - model: function() { - return Admin.Api.create({ - frontendHost: location.hostname, - }); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/application_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/application_route.js deleted file mode 100644 index d42b128ee..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/application_route.js +++ /dev/null @@ -1,21 +0,0 @@ -Admin.ApplicationRoute = Ember.Route.extend({ - actions: { - openModal: function(template) { - this.render(template, { into: 'modal', outlet: 'modalBody' }); - $('.modal').modal({ - // Don't close when the background is clicked or the escape key is hit. - // Not quite ideal, but this ensures the user hits either the "Cancel" - // or "OK" button for the modal forms so we can properly react. If we - // change this we need to determine how to listen for closes via - // keyboard or background clicks and what the behavior should be - // (cancel, or ok?). - backdrop: 'static', - keyboard: false - }); - }, - - closeModal: function() { - this.render('hide_modal', { into: 'modal', outlet: 'modalBody' }); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/config/publish_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/config/publish_route.js deleted file mode 100644 index d10929e32..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/config/publish_route.js +++ /dev/null @@ -1,49 +0,0 @@ -Admin.ConfigPublishRoute = Ember.Route.extend({ - model: function() { - return ic.ajax.request('/api-umbrella/v1/config/pending_changes'); - }, - - setupController: function(controller, model) { - controller.set('model', model); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-config').addClass('active'); - }, - - actions: { - publish: function() { - var form = $('#publish_form'); - - var button = $('#publish_button'); - button.button('loading'); - - ic.ajax.raw({ - url: '/api-umbrella/v1/config/publish', - type: 'POST', - data: form.serialize(), - }).then(_.bind(function() { - button.button('reset'); - new PNotify({ - type: 'success', - title: 'Published', - text: 'Successfully published the configuration
Changes should be live in a few seconds...', - }); - - this.refresh(); - }, this), function(response) { - var message = '

Error

'; - try { - var errors = response.responseJSON.errors; - for(var prop in errors) { - message += prop + ': ' + errors[prop].join(', ') + '
'; - } - } catch(e) { - message = 'An unexpected error occurred: ' + response.responseText; - } - - button.button('reset'); - bootbox.alert(message); - }); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/loading_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/loading_route.js deleted file mode 100644 index 2ea2f3b5a..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/loading_route.js +++ /dev/null @@ -1 +0,0 @@ -Admin.LoadingRoute = Ember.Route.extend({}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/base_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/base_route.js deleted file mode 100644 index 98567ec6e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/base_route.js +++ /dev/null @@ -1,90 +0,0 @@ -Admin.StatsBaseRoute = Ember.Route.extend({ - defaultQueryParams: { - tz: jstz.determine().name(), - search: '', - start_at: moment().subtract(29, 'days').format('YYYY-MM-DD'), - end_at: moment().format('YYYY-MM-DD'), - query: JSON.stringify({ - condition: 'AND', - rules: [{ - field: 'gatekeeper_denied_code', - id: 'gatekeeper_denied_code', - input: 'select', - operator: 'is_null', - type: 'string', - value: null - }] - }) - }, - - model: function(params) { - this.controllerFor('application').set('isLoading', true); - - this.setQueryParams(params); - }, - - setupController: function(controller, model) { - if(!controller.get('query')) { - controller.set('query', this.get('query')); - } - - controller.set('model', model); - - this.controllerFor('application').set('isLoading', false); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-analytics').addClass('active'); - }, - - setQueryParams: function(params) { - var activeQueryParams = {}; - if(params && params.query) { - activeQueryParams = $.deparam(params.query); - } - - _.defaults(activeQueryParams, this.defaultQueryParams); - this.set('activeQueryParams', activeQueryParams); - - var query = this.get('query'); - if(!query) { - query = Ember.Object.create({ params: {} }); - } - - // Wrap setting the parameters in a begin/end transaction and only set - // values that differ. This is to cut down on unneeded observer - // notifications. - query.beginPropertyChanges(); - for(var prop in activeQueryParams) { - if(activeQueryParams.hasOwnProperty(prop)) { - var paramKey = 'params.' + prop; - var existingValue = query.get(paramKey); - var newValue = activeQueryParams[prop]; - - if(newValue !== existingValue) { - query.set(paramKey, newValue); - } - } - } - query.endPropertyChanges(); - - if(!this.get('query')) { - this.set('query', query); - } - }, - - queryChange: function() { - var newQueryParams = this.get('query.params'); - if(newQueryParams && !_.isEmpty(newQueryParams)) { - var activeQueryParams = this.get('activeQueryParams'); - if(!_.isEqual(newQueryParams, activeQueryParams)) { - this.transitionTo('stats.logs', $.param(newQueryParams)); - } - } - }.observes('query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at', 'query.params.beta_analytics'), - - actions: { - error: function() { - bootbox.alert('An unexpected error occurred. Please check your query and try again.'); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/drilldown_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/drilldown_route.js deleted file mode 100644 index 1c7031f95..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/drilldown_route.js +++ /dev/null @@ -1,63 +0,0 @@ -Admin.StatsDrilldownRoute = Admin.StatsBaseRoute.extend({ - init: function() { - _.defaults(this.defaultQueryParams, { - interval: 'day', - prefix: '0/', - }); - }, - - model: function(params) { - this._super(params); - if(this.validateOptions()) { - return Admin.StatsDrilldown.find(this.get('query.params')); - } else { - return {}; - } - }, - - queryChange: function() { - var newQueryParams = this.get('query.params'); - if(newQueryParams && !_.isEmpty(newQueryParams)) { - var activeQueryParams = this.get('activeQueryParams'); - if(!_.isEqual(newQueryParams, activeQueryParams)) { - this.transitionTo('stats.drilldown', $.param(newQueryParams)); - } - } - }.observes('query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at', 'query.params.beta_analytics'), - - validateOptions: function() { - var valid = true; - - var interval = this.get('query.params.interval'); - var start = moment(this.get('query.params.start_at')); - var end = moment(this.get('query.params.end_at')); - - var range = end.unix() - start.unix(); - switch(interval) { - case 'minute': - // 2 days maximum range - if(range > 2 * 24 * 60 * 60) { - valid = false; - bootbox.alert('Your date range is too large for viewing minutely data. Adjust your viewing interval or choose a date range to no more than 2 days.'); - } - - break; - case 'hour': - // 31 day maximum range - if(range > 31 * 24 * 60 * 60) { - valid = false; - bootbox.alert('Your date range is too large for viewing hourly data. Adjust your viewing interval or choose a date range to no more than 31 days.'); - } - - break; - } - - return valid; - }, -}); - -Admin.StatsDrilldownDefaultRoute = Admin.StatsDrilldownRoute.extend({ - renderTemplate: function() { - this.render('stats/drilldown', { controller: 'statsDrilldownDefault' }); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/logs_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/logs_route.js deleted file mode 100644 index 8a26cc704..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/logs_route.js +++ /dev/null @@ -1,62 +0,0 @@ -Admin.StatsLogsRoute = Admin.StatsBaseRoute.extend({ - init: function() { - _.defaults(this.defaultQueryParams, { - interval: 'day', - }); - }, - - model: function(params) { - this._super(params); - if(this.validateOptions()) { - return Admin.StatsLogs.find(this.get('query.params')); - } else { - return {}; - } - }, - - queryChange: function() { - var newQueryParams = this.get('query.params'); - if(newQueryParams && !_.isEmpty(newQueryParams)) { - var activeQueryParams = this.get('activeQueryParams'); - if(!_.isEqual(newQueryParams, activeQueryParams)) { - this.transitionTo('stats.logs', $.param(newQueryParams)); - } - } - }.observes('query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at', 'query.params.beta_analytics'), - - validateOptions: function() { - var valid = true; - - var interval = this.get('query.params.interval'); - var start = moment(this.get('query.params.start_at')); - var end = moment(this.get('query.params.end_at')); - - var range = end.unix() - start.unix(); - switch(interval) { - case 'minute': - // 2 days maximum range - if(range > 2 * 24 * 60 * 60) { - valid = false; - bootbox.alert('Your date range is too large for viewing minutely data. Adjust your viewing interval or choose a date range to no more than 2 days.'); - } - - break; - case 'hour': - // 31 day maximum range - if(range > 31 * 24 * 60 * 60) { - valid = false; - bootbox.alert('Your date range is too large for viewing hourly data. Adjust your viewing interval or choose a date range to no more than 31 days.'); - } - - break; - } - - return valid; - }, -}); - -Admin.StatsLogsDefaultRoute = Admin.StatsLogsRoute.extend({ - renderTemplate: function() { - this.render('stats/logs', { controller: 'statsLogsDefault' }); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/map_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/map_route.js deleted file mode 100644 index 4128aa906..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/map_route.js +++ /dev/null @@ -1,28 +0,0 @@ -Admin.StatsMapRoute = Admin.StatsBaseRoute.extend({ - init: function() { - _.defaults(this.defaultQueryParams, { - region: 'world', - }); - }, - - model: function(params) { - this._super(params); - return Admin.StatsMap.find(this.get('query.params')); - }, - - queryChange: function() { - var newQueryParams = this.get('query.params'); - if(newQueryParams && !_.isEmpty(newQueryParams)) { - var activeQueryParams = this.get('activeQueryParams'); - if(!_.isEqual(newQueryParams, activeQueryParams)) { - this.transitionTo('stats.map', $.param(newQueryParams)); - } - } - }.observes('query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.region', 'query.params.beta_analytics'), -}); - -Admin.StatsMapDefaultRoute = Admin.StatsMapRoute.extend({ - renderTemplate: function() { - this.render('stats/map', { controller: 'statsMapDefault' }); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/users_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/users_route.js deleted file mode 100644 index 08f25cf4e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/stats/users_route.js +++ /dev/null @@ -1,22 +0,0 @@ -Admin.StatsUsersRoute = Admin.StatsBaseRoute.extend({ - model: function(params) { - this._super(params); - return {}; - }, - - queryChange: function() { - var newQueryParams = this.get('query.params'); - if(newQueryParams && !_.isEmpty(newQueryParams)) { - var activeQueryParams = this.get('activeQueryParams'); - if(!_.isEqual(newQueryParams, activeQueryParams)) { - this.transitionTo('stats.users', $.param(newQueryParams)); - } - } - }.observes('query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.beta_analytics'), -}); - -Admin.StatsUsersDefaultRoute = Admin.StatsUsersRoute.extend({ - renderTemplate: function() { - this.render('stats/users', { controller: 'statsUsersDefault' }); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/base_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/base_route.js deleted file mode 100644 index 22172a1e7..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/base_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.WebsiteBackendsBaseRoute = Ember.Route.extend({ - setupController: function(controller, model) { - controller.set('model', model); - - $('ul.nav li').removeClass('active'); - $('ul.nav li.nav-config').addClass('active'); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/edit_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/edit_route.js deleted file mode 100644 index c3031de36..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/edit_route.js +++ /dev/null @@ -1,10 +0,0 @@ -Admin.WebsiteBackendsEditRoute = Admin.WebsiteBackendsBaseRoute.extend({ - model: function(params) { - // Clear the record cache, so this is always fetched from the server (to - // account for two users simultaneously editing the same record). - Admin.WebsiteBackend.clearCache(); - - return Admin.WebsiteBackend.find(params.websiteBackendId); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/index_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/index_route.js deleted file mode 100644 index fff47781f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/index_route.js +++ /dev/null @@ -1,2 +0,0 @@ -Admin.WebsiteBackendsIndexRoute = Admin.WebsiteBackendsBaseRoute.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/new_route.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/new_route.js deleted file mode 100644 index a05eb596d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/routes/website_backends/new_route.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.WebsiteBackendsNewRoute = Admin.WebsiteBackendsBaseRoute.extend({ - model: function() { - return Admin.WebsiteBackend.create({ - serverPort: 80, - }); - }, -}); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/server_side_loader.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/server_side_loader.js new file mode 100644 index 000000000..86fb57d0a --- /dev/null +++ b/src/api-umbrella/web-app/app/assets/javascripts/admin/server_side_loader.js @@ -0,0 +1,3 @@ +//= require i18n.js +//= require i18n/translations.js +//= require admin/_common_validations.js diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/_form.hbs deleted file mode 100644 index fe2d41f32..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/_form.hbs +++ /dev/null @@ -1,63 +0,0 @@ -{{error-messages model=model}} - -{{#form-for model}} -
- {{input name - label='Group Name' - inputConfig='class:span6'}} - -
-
- -
-
- {{view Admin.CheckboxListView checkedValuesBinding='apiScopeIds' contentBinding='apiScopeOptions'}} -
-
- -
-
- -
-
- {{view Admin.CheckboxListView checkedValuesBinding='permissionIds' contentBinding='permissionOptions'}} -
-
- - {{#if id}} -
-
- -
-
- {{#if admins}} -
    - {{#each admins}} -
  • {{#link-to 'admins.edit' id}}{{username}}{{/link-to}} (Last Login: {{#if last_sign_in_at}}{{formatDate last_sign_in_at}}{{else}}Never{{/if}})
  • - {{/each}} -
- {{else}} - None - {{/if}} -
-
- {{/if}} -
- -
-
- -
-
- {{#if id}} - Created: {{formatDate createdAt}} by {{creator.username}}
- Last Updated: {{formatDate updatedAt}} by {{updater.username}}
- {{/if}} -
-
- {{#if id}} - - {{/if}} -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/edit.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/edit.hbs deleted file mode 100644 index 276e5fc9d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/edit.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Edit Admin Group

-{{partial "admin_groups/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/index.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/index.hbs deleted file mode 100644 index cf6ed930f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/index.hbs +++ /dev/null @@ -1,9 +0,0 @@ -

Admin Groups

- - - -
- {{view Admin.AdminGroupsTableView dataBinding='model.logs' queryBinding='query'}} -
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/new.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/new.hbs deleted file mode 100644 index 3bb247524..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admin_groups/new.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Add Admin Group

-{{partial "admin_groups/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/_form.hbs deleted file mode 100644 index 8c9134be2..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/_form.hbs +++ /dev/null @@ -1,83 +0,0 @@ -{{error-messages model=model}} - -{{#form-for model}} -
- User Info - - {{input username - label='Username' - inputConfig='class:span6'}} - - {{input notes as='text' - label='Notes' - inputConfig='class:span6'}} - - {{#if email}} -
-
E-mail
-
{{email}}
-
- {{/if}} - {{#if name}} -
-
Name
-
{{name}}
-
- {{/if}} -
- - {{#if authenticationToken}} -
- Admin API Access - -
-
Admin API Token
-
{{authenticationToken}}
-
-
- {{/if}} - -
- Permissions - -
-
- -
-
- {{view Admin.CheckboxListView checkedValuesBinding='groupIds' contentBinding='groupOptions'}} -
-
- - {{#if currentAdmin.superuser}} -
-
- -
-
- {{/if}} - -
- -
-
- -
-
- {{#if id}} - Created: {{formatDate createdAt}} by {{creator.username}}
- Last Updated: {{formatDate updatedAt}} by {{updater.username}}
- Last Login: {{formatDate lastSignInAt}} from {{lastSignInIp}} via {{lastSignInProvider}}
- Logged in: {{signInCount}} times
- {{/if}} -
-
- {{#if id}} - - {{/if}} -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/edit.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/edit.hbs deleted file mode 100644 index 2975beaaf..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/edit.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Edit Admin

-{{partial "admins/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/index.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/index.hbs deleted file mode 100644 index 9449f1440..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/index.hbs +++ /dev/null @@ -1,13 +0,0 @@ -

Admins

- - - -
- {{view Admin.AdminsTableView dataBinding='model.logs' queryBinding='query'}} -
- - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/new.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/new.hbs deleted file mode 100644 index 15cb985b1..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/admins/new.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Add Admin

-{{partial "admins/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/_form.hbs deleted file mode 100644 index cdb83dc53..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/_form.hbs +++ /dev/null @@ -1,34 +0,0 @@ -{{error-messages model=model}} - -{{#form-for model}} -
- {{input name - label='Name' - inputConfig='class:span6'}} - - {{input host - label='Host' - inputConfig='class:span6'}} - - {{input pathPrefix - label='Path Prefix' - inputConfig='class:span6'}} -
- -
-
- -
-
- {{#if id}} - Created: {{formatDate createdAt}} by {{creator.username}}
- Last Updated: {{formatDate updatedAt}} by {{updater.username}}
- {{/if}} -
-
- {{#if id}} - - {{/if}} -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/edit.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/edit.hbs deleted file mode 100644 index 4afcf8899..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/edit.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Edit API Scope

-{{partial "api_scopes/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/index.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/index.hbs deleted file mode 100644 index 7f019222b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/index.hbs +++ /dev/null @@ -1,9 +0,0 @@ -

API Scopes

- - - -
- {{view Admin.ApiScopesTableView dataBinding='model.logs' queryBinding='query'}} -
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/new.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/new.hbs deleted file mode 100644 index 86320593a..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_scopes/new.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Add API Scope

-{{partial "api_scopes/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/_form.hbs deleted file mode 100644 index 763d5e835..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/_form.hbs +++ /dev/null @@ -1,120 +0,0 @@ -{{error-messages model=model}} - -{{#form-for model}} -
- User Info - - {{input email - label='E-mail' - inputConfig='class:span6'}} - {{input firstName - label='First Name' - inputConfig='class:span6'}} - {{input lastName - label='Last Name' - inputConfig='class:span6'}} - {{input useDescription as='text' - label='Purpose' - inputConfig='rows:3;class:span6'}} - - {{#if id}} -
-
Signed Up
-
{{formatDate createdAt}}
-
-
-
API Key
-
- {{#if apiKey}} - {{apiKeyPreview}} {{t 'admin.reveal_action'}} - {{else}} - {{apiKeyPreview}} - {{/if}} -
-
-
-
User ID
-
- {{id}} -
-
-
-
Registration Source
-
{{registrationSource}}
-
- {{else}} -
-
- -
-
-
-
- -
-
- {{/if}} -
- -
- Rate Limiting - {{render 'apis/settings_rate_limit_fields' settings}} - - {{input throttleByIp as='select' - value='throttleByIp' - label='Limit By' - collection='throttleByIpOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} -
- -
- Permissions - - {{input rolesString as='selectize' - collectionBinding='roleOptions' - optionValuePath='id' - optionLabelPath='id' - class='row-fluid' - label='Roles'}} - - {{render 'apis/settings_allowed_ips_fields' settings}} - {{render 'apis/settings_allowed_referers_fields' settings}} - - {{input enabled as='select' - value='enabled' - label='Account Enabled' - collection='enabledOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} -
- -
-
- -
-
- Created: {{formatDate createdAt}}{{#if creator}} by {{creator.username}}{{/if}}
- Last Updated: {{formatDate updatedAt}}{{#if updater}} by {{updater.username}}{{/if}}
- E-mail Verified: {{emailVerified}}
- {{#if registrationIp}} - Registration IP: {{registrationIp}}
- {{/if}} - {{#if registrationUserAgent}} - Registration User Agent: {{registrationUserAgent}}
- {{/if}} - {{#if registrationReferer}} - Registration Referer: {{registrationReferer}}
- {{/if}} - {{#if registrationOrigin}} - Registration Origin: {{registrationOrigin}}
- {{/if}} -
-
-{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/edit.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/edit.hbs deleted file mode 100644 index c111ac1e7..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/edit.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Edit API User

-{{partial "api_users/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/index.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/index.hbs deleted file mode 100644 index c835dc84f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/index.hbs +++ /dev/null @@ -1,9 +0,0 @@ -

API Users

- - - -
- {{view Admin.ApiUsersTableView dataBinding='model.logs' queryBinding='query'}} -
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/new.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/new.hbs deleted file mode 100644 index 63fd04547..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/api_users/new.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Add API User

-{{partial "api_users/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/_form.hbs deleted file mode 100644 index 948fb9d94..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/_form.hbs +++ /dev/null @@ -1,209 +0,0 @@ -{{error-messages model=model}} - -{{#form-for model}} - {{input name - label=(t 'mongoid.attributes.api.name') - inputConfig='class:span6'}} - -
- {{t 'admin.api.servers.legend'}} -

{{t 'admin.api.servers.note'}}

- {{input backendProtocol as='select' - label=(t 'mongoid.attributes.api.backend_protocol') - value='backendProtocol' - collection='backendProtocolOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} - - - - - - - - - - {{#if servers}} - {{#each servers}} - - - - - {{/each}} - {{else}} - - {{/if}} - -
{{t 'admin.api.servers.server'}}
{{../backendProtocol}}://{{hostWithPort}} - {{t 'admin.edit'}} - {{t 'admin.remove'}} -
{{t 'admin.api.servers.empty_list' add=(t 'admin.api.servers.add')}}
- -
- -
- {{t 'admin.api.host.legend'}} -

{{t 'admin.api.host.note'}}

-
-
- {{input frontendHost - label='Frontend Host' - inputConfig='class:span12'}} -
-
- -
rewrite to
-
-
- {{input backendHost - label='Backend Host' - placeholder='api.example.com' - inputConfig='class:span12'}} -
-
-
- -
- {{t 'admin.api.url_matches.legend'}} -

{{t 'admin.api.url_matches.note'}}

- {{render 'apis/url_matches' urlMatches}} -
- -
- -
- {{#with settings}} - {{input appendQueryString - class='row-fluid' - label='Append Query String Parameters' - placeholder='param1=value¶m2=value' - inputConfig='class:span12'}} - {{input headersString as='text' - class='row-fluid' - label='Set Request Headers' - placeholder='X-Example-Header: value' - inputConfig='class:span12'}} - {{input httpBasicAuth - class='row-fluid' - label='HTTP Basic Authentication' - placeholder='username:password' - inputConfig='class:span12'}} - {{/with}} - - {{api-settings-fields model=settings}} -
-
- -
- -
-

{{t 'admin.api.sub_settings.note'}}

- {{render 'apis/sub_settings' subSettings}} -
-
- -
- -
-

{{t 'admin.api.rewrites.note'}}

- {{render 'apis/rewrites' rewrites}} -
-
- -
- -
- {{input balanceAlgorithm as='select' - value='balanceAlgorithm' - collection='balanceAlgorithmOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} - - {{#with settings}} - {{#with errorTemplates}} -

{{t 'mongoid.attributes.api/settings.error_templates'}}

- {{input json as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_templates_json' - tooltipTranslation='admin.api.settings.error_templates_json_tooltip_markdown' - inputConfig='data-ace-mode:json'}} - - {{input xml as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_templates_xml' - tooltipTranslation='admin.api.settings.error_templates_xml_tooltip_markdown' - inputConfig='data-ace-mode:xml'}} - - {{input csv as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_templates_csv' - tooltipTranslation='admin.api.settings.error_templates_csv_tooltip_markdown' - inputConfig='data-ace-mode:text'}} - {{/with}} - - {{#with errorDataYamlStrings}} -

{{t 'mongoid.attributes.api/settings.error_data'}}

- - {{input common as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_data_common' - tooltipTranslation='admin.api.settings.error_data_common_tooltip_markdown' - inputConfig='data-ace-mode:yaml'}} - - {{input api_key_missing as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_data_api_key_missing' - tooltipTranslation='admin.api.settings.error_data_api_key_missing_tooltip_markdown' - inputConfig='data-ace-mode:yaml'}} - - {{input api_key_invalid as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_data_api_key_invalid' - tooltipTranslation='admin.api.settings.error_data_api_key_invalid_tooltip_markdown' - inputConfig='data-ace-mode:yaml'}} - - {{input api_key_disabled as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_data_api_key_disabled' - tooltipTranslation='admin.api.settings.error_data_api_key_disabled_tooltip_markdown' - inputConfig='data-ace-mode:yaml'}} - - {{input api_key_unauthorized as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_data_api_key_unauthorized' - tooltipTranslation='admin.api.settings.error_data_api_key_unauthorized_tooltip_markdown' - inputConfig='data-ace-mode:yaml'}} - - {{input over_rate_limit as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_data_over_rate_limit' - tooltipTranslation='admin.api.settings.error_data_over_rate_limit_tooltip_markdown' - inputConfig='data-ace-mode:yaml'}} - - {{input https_required as='ace' - class='row-fluid' - labelTranslation='admin.api.settings.error_data_https_required' - tooltipTranslation='admin.api.settings.error_data_https_required_tooltip_markdown' - inputConfig='data-ace-mode:yaml'}} - {{/with}} - {{/with}} -
-
- -
-
- -
-
- {{#if id}} - Created: {{formatDate createdAt}} by {{creator.username}}
- Last Updated: {{formatDate updatedAt}} by {{updater.username}}
- {{/if}} -
-
- {{#if id}} - - {{/if}} -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/edit.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/edit.hbs deleted file mode 100644 index 4dc916c5d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/edit.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Edit Form

-{{partial "apis/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/index.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/index.hbs deleted file mode 100644 index d2b47f858..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/index.hbs +++ /dev/null @@ -1,12 +0,0 @@ -

API Backends

- - - -
- {{view Admin.ApisTableView}} -
-
- -
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/new.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/new.hbs deleted file mode 100644 index 7796974b0..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/new.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Add API

-{{partial "apis/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/rewrite_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/rewrite_form.hbs deleted file mode 100644 index 621858938..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/rewrite_form.hbs +++ /dev/null @@ -1,38 +0,0 @@ -{{#form-for model}} - - -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/rewrites.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/rewrites.hbs deleted file mode 100644 index 46fb70c1d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/rewrites.hbs +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - {{#if model}} - {{#each}} - - - - - - - - - {{/each}} - {{else}} - - {{/if}} - -
Matching TypeHTTP MethodFromTo
{{matcherType}}{{httpMethod}}{{frontendMatcher}}{{backendReplacement}} - {{t 'admin.edit'}} - {{t 'admin.remove'}} -
{{t 'admin.api.rewrites.empty_list' add=(t 'admin.api.rewrites.add')}}
-
-
- -
-
- {{#if reorderable}} - - {{/if}} -
-
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/server_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/server_form.hbs deleted file mode 100644 index c06e669c2..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/server_form.hbs +++ /dev/null @@ -1,18 +0,0 @@ -{{#form-for model}} - - -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_allowed_ips_fields.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_allowed_ips_fields.hbs deleted file mode 100644 index 4444c82ae..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_allowed_ips_fields.hbs +++ /dev/null @@ -1,6 +0,0 @@ -{{input allowedIpsString as='text' - class='row-fluid' - labelTranslation='mongoid.attributes.api/settings.allowed_ips' - tooltipTranslation='admin.api.settings.allowed_ips_tooltip_markdown' - placeholder='10.0.0.0/8' - inputConfig='class:span12'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_allowed_referers_fields.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_allowed_referers_fields.hbs deleted file mode 100644 index 960de110e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_allowed_referers_fields.hbs +++ /dev/null @@ -1,6 +0,0 @@ -{{input allowedReferersString as='text' - class='row-fluid' - labelTranslation='mongoid.attributes.api/settings.allowed_referers' - tooltipTranslation='admin.api.settings.allowed_referers_tooltip_markdown' - placeholder='*.example.com/*' - inputConfig='class:span12'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_rate_limit_fields.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_rate_limit_fields.hbs deleted file mode 100644 index 1f5a32867..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/settings_rate_limit_fields.hbs +++ /dev/null @@ -1,95 +0,0 @@ -{{input rateLimitMode as='select' - value='rateLimitMode' - label='Rate Limit' - collection='rateLimitModeOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} - -{{#if isRateLimitModeCustom}} -
-
- - - - - - - - - - - - - {{#if rateLimits}} - {{#each rateLimits}} - - - - - - - - - {{/each}} - {{else}} - - {{/if}} - -
DurationLimit ByLimit - Primary - -
- {{input-field durationInUnits inputConfig='class:input-small rate-limit-duration-in-units'}} - - {{input-field durationUnits as='select' - value='durationUnits' - collection='controller.rateLimitDurationUnitOptions' - inputConfig='class:input-small rate-limit-duration-units' - optionValuePath='content.id' - optionLabelPath='content.name'}} - - {{input-field limitBy as='select' - value='limitBy' - collection='controller.rateLimitLimitByOptions' - inputConfig='class:input-small rate-limit-limit-by' - optionValuePath='content.id' - optionLabelPath='content.name'}} - -
- {{input-field limit inputConfig='class:input-small rate-limit-limit'}} - requests -
-
- {{view Admin.BooleanRadioButtonView - nameBinding='controller.uniqueSettingsId' - selectionBinding='responseHeaders' - value='true' - class='rate-limit-response-headers'}} - - Remove -
No custom rate limits have been added yet. Click "Add Rate Limit" below to get started.
- -
-
-{{/if}} - -{{#if disableApiKey}} - {{input anonymousRateLimitBehavior as='select' - value='anonymousRateLimitBehavior' - label='Anonymous Rate Limit Behavior' - collection='anonymousRateLimitBehaviorOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} - - {{input authenticatedRateLimitBehavior as='select' - value='authenticatedRateLimitBehavior' - label='Authenticated Rate Limit Behavior' - collection='authenticatedRateLimitBehaviorOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} -{{/if}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/sub_settings.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/sub_settings.hbs deleted file mode 100644 index bcf95598d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/sub_settings.hbs +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - {{#if model}} - {{#each}} - - - - - - - {{/each}} - {{else}} - - {{/if}} - -
HTTP MethodURL Matcher
{{httpMethod}}{{regex}} - Edit - Remove -
No sub-URL request settings have been added yet. Click "Add URL Settings" below to get started.
-
-
- -
-
- {{#if reorderable}} - - {{/if}} -
-
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/sub_settings_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/sub_settings_form.hbs deleted file mode 100644 index 0982a17ae..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/sub_settings_form.hbs +++ /dev/null @@ -1,25 +0,0 @@ -{{#form-for model}} - - -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/url_match_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/url_match_form.hbs deleted file mode 100644 index 27ea05f23..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/url_match_form.hbs +++ /dev/null @@ -1,34 +0,0 @@ -{{#form-for model}} - - -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/url_matches.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/url_matches.hbs deleted file mode 100644 index 920010c4e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/apis/url_matches.hbs +++ /dev/null @@ -1,39 +0,0 @@ -
- - - - - - - - - - - {{#if model}} - {{#each}} - - - - - - - {{/each}} - {{else}} - - {{/if}} - -
Frontend PrefixBackend Prefix
{{frontendPrefix}}{{backendPrefixWithDefault}} - {{t 'admin.edit'}} - {{t 'admin.remove'}} -
{{t 'admin.api.url_matches.empty_list' add=(t 'admin.api.url_matches.add')}}
-
-
- -
-
- {{#if reorderable}} - - {{/if}} -
-
-
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/application.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/application.hbs deleted file mode 100644 index 4141d5c1e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/application.hbs +++ /dev/null @@ -1,2 +0,0 @@ -{{outlet}} -{{render 'modal'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/components/api-settings-fields.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/components/api-settings-fields.hbs deleted file mode 100644 index 400f68f03..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/components/api-settings-fields.hbs +++ /dev/null @@ -1,78 +0,0 @@ -{{input requireHttps as='select' - value='model.requireHttps' - class='row-fluid' - labelTranslation='admin.api.settings.require_https' - tooltipClass='qtip-wide' - tooltipTranslation='admin.api.settings.require_https_tooltip_markdown' - collection='requireHttpsOptions' - optionValuePath='content.id' - optionLabelPath='content.name' - inputConfig='class:span12'}} -{{input disableApiKey as='select' - value='model.disableApiKey' - class='row-fluid' - labelTranslation='admin.api.settings.disable_api_key' - collection='disableApiKeyOptions' - optionValuePath='content.id' - optionLabelPath='content.name' - inputConfig='class:span12'}} -{{input apiKeyVerificationLevel as='select' - value='model.apiKeyVerificationLevel' - class='row-fluid' - labelTranslation='admin.api.settings.api_key_verification_level' - collection='apiKeyVerificationLevelOptions' - optionValuePath='content.id' - optionLabelPath='content.name' - inputConfig='class:span12'}} -{{input model.requiredRolesString as='selectize' - collectionBinding='roleOptions' - optionValuePath='id' - optionLabelPath='id' - class='row-fluid' - labelTranslation='admin.api.settings.required_roles' - tooltipTranslation='admin.api.settings.required_roles_tooltip_markdown'}} -{{#if isSubSettings}} -
-
-
-
- -
-
-{{/if}} - -
-
- - {{tooltip-field title=(t 'admin.api.settings.pass_api_key_tooltip_markdown')}} -
-
- - -
-
- -{{render 'apis/settings_rate_limit_fields' model}} - -{{input model.defaultResponseHeadersString as='text' - class='row-fluid' - labelTranslation='admin.api.settings.default_response_headers' - placeholder='X-Example-Header: value' - tooltipTranslation='admin.api.settings.default_response_headers_tooltip_markdown' - inputConfig='class:span12'}} -{{input model.overrideResponseHeadersString as='text' - class='row-fluid' - labelTranslation='admin.api.settings.override_response_headers' - placeholder='X-Example-Header: value' - tooltipTranslation='admin.api.settings.override_response_headers_tooltip_markdown' - inputConfig='class:span12'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/components/error-messages.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/components/error-messages.hbs deleted file mode 100644 index 88e7ef508..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/components/error-messages.hbs +++ /dev/null @@ -1,12 +0,0 @@ -
- {{#if messages.length}} -
-

Oops! Please check the errors below:

-
    - {{#each messages}} -
  • {{{.}}}
  • - {{/each}} -
-
- {{/if}} -
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/config/publish.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/config/publish.hbs deleted file mode 100644 index 8bbc3683e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/config/publish.hbs +++ /dev/null @@ -1,64 +0,0 @@ -

Publish Configuration Changes

- -{{#if hasChanges}} -
- {{#if model.config.apis.new}} -
- {{model.config.apis.new.length}} New API Backends - {{render "config/publish_record" model.config.apis.new category="apis"}} -
- {{/if}} - - {{#if model.config.website_backends.new}} -
- {{model.config.website_backends.new.length}} New Website Backends - {{render "config/publish_record" model.config.website_backends.new category="website_backends"}} -
- {{/if}} - - {{#if model.config.apis.modified}} -
- {{model.config.apis.modified.length}} Modified API Backends - {{render "config/publish_record" model.config.apis.modified category="apis"}} -
- {{/if}} - - {{#if model.config.website_backends.modified}} -
- {{model.config.website_backends.modified.length}} Modified Website Backends - {{render "config/publish_record" model.config.website_backends.modified category="website_backends"}} -
- {{/if}} - - {{#if model.config.apis.deleted}} -
- {{model.config.apis.deleted.length}} Deleted API Backends - {{render "config/publish_record" model.config.apis.deleted category="apis"}} -
- {{/if}} - - {{#if model.config.website_backends.deleted}} -
- {{model.config.website_backends.deleted.length}} Deleted Website Backends - {{render "config/publish_record" model.config.website_backends.deleted category="website_backends"}} -
- {{/if}} - - - -
- -
-
-{{else}} -
-
-
- Published configuration is up to date
- Recently published changes should be live within a few seconds -
-
-
-{{/if}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/config/publish_record.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/config/publish_record.hbs deleted file mode 100644 index 815423457..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/config/publish_record.hbs +++ /dev/null @@ -1,34 +0,0 @@ -{{#if model}} - - - - - - - - - {{#each model}} - - - - - - - - - {{/each}} - -
Publish?Name
- - - - - - {{name}}
View Config Differences
-{{else}} -
None
-{{/if}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/loading.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/loading.hbs deleted file mode 100644 index c0a4f84ce..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/loading.hbs +++ /dev/null @@ -1 +0,0 @@ -{{view Admin.LoadingOverlayView isLoading=true}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/modal.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/modal.hbs deleted file mode 100644 index 970b0c42b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/modal.hbs +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/_facet_table.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/_facet_table.hbs deleted file mode 100644 index 6088f227e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/_facet_table.hbs +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - {{#each view.data}} - - - - - - {{/each}} - - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/_query_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/_query_form.hbs deleted file mode 100644 index f8ea45b5f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/_query_form.hbs +++ /dev/null @@ -1,183 +0,0 @@ -
-
- -
- {{#if view.enableBetaAnalytics}} - - {{/if}} - - {{#if view.enableInterval}} -
- - - - - -
- {{/if}} - - -
-
-
-
- -
-
-
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/drilldown.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/drilldown.hbs deleted file mode 100644 index 4230c5dad..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/drilldown.hbs +++ /dev/null @@ -1,20 +0,0 @@ -{{view Admin.StatsQueryFormView enableInterval=true}} - -{{view Admin.StatsDrilldownChartView dataBinding='hits_over_time'}} - -{{#each breadcrumbs}} - {{#if linkQuery}} - {{#link-to 'stats.drilldown' linkQuery}}{{name}}{{/link-to}} / - {{else}} - {{name}} - {{/if}} -{{/each}} - -
- {{view Admin.StatsDrilldownTableView modelBinding='model'}} -
- - -{{view Admin.LoadingOverlayView isLoadingBinding='controllers.application.isLoading'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/logs.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/logs.hbs deleted file mode 100644 index 3cfd2ecd8..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/logs.hbs +++ /dev/null @@ -1,41 +0,0 @@ -{{view Admin.StatsQueryFormView enableInterval=true}} - -{{view Admin.StatsIntervalChartView dataBinding='model.hits_over_time'}} - -
-
- {{formatNumber model.stats.total_hits}} - {{inflect "hit" model.stats.total_hits}} -
-
- {{formatNumber model.stats.total_users}} - unique {{inflect "user" model.stats.total_users}} - - {{view Admin.StatsFacetTableView - dataBinding='model.aggregations.users' - facetTerm='user_email' - toggleLabel='view top users'}} -
-
- {{formatNumber model.stats.total_ips}} - unique ip {{inflect "address" model.stats.total_ips}} - - {{view Admin.StatsFacetTableView - dataBinding='model.aggregations.ips' - facetTerm='request_ip' - toggleLabel='view top ips'}} -
-
- {{formatNumber model.stats.average_response_time}} ms - average response time -
-
- -
- {{view Admin.LogsTableView dataBinding='model.logs' queryBinding='query'}} -
- - -{{view Admin.LoadingOverlayView isLoadingBinding='controllers.application.isLoading'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/map.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/map.hbs deleted file mode 100644 index aca874e63..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/map.hbs +++ /dev/null @@ -1,22 +0,0 @@ -{{view Admin.StatsQueryFormView}} - -
- {{#each breadcrumbs}} - {{#if linkQuery}} - {{#link-to 'stats.map' linkQuery}}{{name}}{{/link-to}} / - {{else}} - {{name}} - {{/if}} - {{/each}} - - {{view Admin.StatsMapGeoView modelBinding='model'}} -
- -
- {{view Admin.StatsMapTableView modelBinding='model'}} -
- - -{{view Admin.LoadingOverlayView isLoadingBinding='controllers.application.isLoading'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/users.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/users.hbs deleted file mode 100644 index f8cca5253..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/stats/users.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{view Admin.StatsQueryFormView}} - -
- {{view Admin.StatsUsersTableView}} -
- - -{{view Admin.LoadingOverlayView isLoadingBinding='controllers.application.isLoading'}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/_form.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/_form.hbs deleted file mode 100644 index 196299ce5..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/_form.hbs +++ /dev/null @@ -1,43 +0,0 @@ -{{error-messages model=model}} - -{{#form-for model}} -
- {{input frontendHost - label='Frontend Host' - placeholder='example.com' - inputConfig='class:span6'}} - - {{input backendProtocol as='select' - label=(t 'mongoid.attributes.api.backend_protocol') - value='backendProtocol' - collection='backendProtocolOptions' - optionValuePath='content.id' - optionLabelPath='content.name'}} - - {{input serverHost - label='Backend Server' - placeholder='example.github.io' - inputConfig='class:span6'}} - - {{input serverPort - label='Backend Port' - inputConfig='class:span2'}} -
- -
-
- -
-
- {{#if id}} - Created: {{formatDate createdAt}} by {{creator.username}}
- Last Updated: {{formatDate updatedAt}} by {{updater.username}}
- {{/if}} -
-
- {{#if id}} - - {{/if}} -{{/form-for}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/edit.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/edit.hbs deleted file mode 100644 index 0cb87040b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/edit.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Edit Website Backend

-{{partial "website_backends/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/index.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/index.hbs deleted file mode 100644 index 2c9561f76..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/index.hbs +++ /dev/null @@ -1,9 +0,0 @@ -

Website Backends

- - - -
- {{view Admin.WebsiteBackendsTableView}} -
diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/new.hbs b/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/new.hbs deleted file mode 100644 index 245e1a33b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/templates/website_backends/new.hbs +++ /dev/null @@ -1,2 +0,0 @@ -

Add Website Backend

-{{partial "website_backends/form"}} diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/admin_groups/table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/admin_groups/table_view.js deleted file mode 100644 index 8eaf21981..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/admin_groups/table_view.js +++ /dev/null @@ -1,49 +0,0 @@ -Admin.AdminGroupsTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.$().DataTable({ - serverSide: true, - ajax: '/api-umbrella/v1/admin_groups.json', - pageLength: 50, - order: [[0, 'asc']], - columns: [ - { - data: 'name', - title: 'Name', - defaultContent: '-', - render: _.bind(function(name, type, data) { - if(type === 'display' && name && name !== '-') { - var link = '#/admin_groups/' + data.id + '/edit'; - return '' + _.escape(name) + ''; - } - - return name; - }, this), - }, - { - data: 'api_scope_display_names', - title: 'API Scopes', - orderable: false, - render: Admin.DataTablesHelpers.renderListEscaped, - }, - { - data: 'permission_display_names', - title: 'Access', - defaultContent: '-', - orderable: false, - render: Admin.DataTablesHelpers.renderListEscaped, - }, - { - data: 'admin_usernames', - title: 'Admins', - defaultContent: '-', - orderable: false, - render: Admin.DataTablesHelpers.renderListEscaped, - } - ] - }); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/admins/table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/admins/table_view.js deleted file mode 100644 index 494355b57..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/admins/table_view.js +++ /dev/null @@ -1,70 +0,0 @@ -Admin.AdminsTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - var dataTable = this.$().DataTable({ - serverSide: true, - ajax: '/api-umbrella/v1/admins.json', - pageLength: 50, - order: [[0, 'asc']], - columns: [ - { - data: 'username', - name: 'Username', - title: 'Username', - defaultContent: '-', - render: _.bind(function(email, type, data) { - if(type === 'display' && email && email !== '-') { - var link = '#/admins/' + data.id + '/edit'; - return '' + _.escape(email) + ''; - } - - return email; - }, this), - }, - { - data: 'email', - name: 'E-mail', - title: 'E-mail', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'name', - name: 'Name', - title: 'Name', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'group_names', - name: 'Groups', - title: 'Groups', - orderable: false, - render: Admin.DataTablesHelpers.renderListEscaped, - }, - { - data: 'last_sign_in_at', - type: 'date', - name: 'Last Signed In', - title: 'Last Signed In', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderTime, - }, - { - data: 'created_at', - type: 'date', - name: 'Created', - title: 'Created', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderTime, - } - ] - }); - dataTable.on('draw.dt', function() { - this.get('controller').send('paramsChange', dataTable.ajax.params()); - }.bind(this)); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_scopes/table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_scopes/table_view.js deleted file mode 100644 index 9fbccf29d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_scopes/table_view.js +++ /dev/null @@ -1,41 +0,0 @@ -Admin.ApiScopesTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.$().DataTable({ - serverSide: true, - ajax: '/api-umbrella/v1/api_scopes.json', - pageLength: 50, - order: [[0, 'asc']], - columns: [ - { - data: 'name', - title: 'Name', - defaultContent: '-', - render: _.bind(function(name, type, data) { - if(type === 'display' && name && name !== '-') { - var link = '#/api_scopes/' + data.id + '/edit'; - return '' + _.escape(name) + ''; - } - - return name; - }, this), - }, - { - data: 'host', - title: 'Host', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'path_prefix', - title: 'Path Prefix', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - } - ] - }); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_users/edit_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_users/edit_view.js deleted file mode 100644 index ba791e795..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_users/edit_view.js +++ /dev/null @@ -1,24 +0,0 @@ -Admin.ApiUsersEditView = Ember.View.extend({ - willDestroyElement: function() { - if(this.apiKeyHideTimeout) { - clearTimeout(this.apiKeyHideTimeout); - } - }, - - actions: { - apiKeyRevealToggle: function() { - var $key = this.$().find('.api-key'); - var $toggle = this.$().find('.api-key-reveal-toggle'); - - if($key.data('revealed') === 'true') { - $key.text($key.data('api-key-preview')); - $key.data('revealed', 'false'); - $toggle.text(polyglot.t('admin.reveal_action')); - } else { - $key.text($key.data('api-key')); - $key.data('revealed', 'true'); - $toggle.text(polyglot.t('admin.hide_action')); - } - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_users/table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_users/table_view.js deleted file mode 100644 index d2d1cf24a..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/api_users/table_view.js +++ /dev/null @@ -1,67 +0,0 @@ -Admin.ApiUsersTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.$().DataTable({ - serverSide: true, - ajax: '/api-umbrella/v1/users.json', - pageLength: 50, - order: [[4, 'desc']], - columns: [ - { - data: 'email', - title: 'E-mail', - defaultContent: '-', - render: _.bind(function(email, type, data) { - if(type === 'display' && email && email !== '-') { - var link = '#/api_users/' + data.id + '/edit'; - return '' + _.escape(email) + ''; - } - - return email; - }, this), - }, - { - data: 'first_name', - title: 'First Name', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'last_name', - title: 'Last Name', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'use_description', - title: 'Purpose', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'created_at', - type: 'date', - title: 'Created', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderTime, - }, - { - data: 'registration_source', - title: 'Registration Source', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'api_key_preview', - title: 'API Key', - defaultContent: '-', - orderable: false, - render: Admin.DataTablesHelpers.renderEscaped, - }, - ] - }); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/index_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/index_view.js deleted file mode 100644 index 1dea8491e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/index_view.js +++ /dev/null @@ -1,14 +0,0 @@ -Admin.ApisIndexView = Ember.View.extend({ - handleReorderChange: function() { - var $container = this.$(); - if($container) { - var $buttonText = this.$().find('.reorder-button-text'); - if(this.get('controller.reorderActive')) { - $buttonText.data('originalText', $buttonText.text()); - $buttonText.text('Done'); - } else { - $buttonText.text($buttonText.data('originalText')); - } - } - }.observes('controller.reorderActive'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/server_form_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/server_form_view.js deleted file mode 100644 index 9959be8f7..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/server_form_view.js +++ /dev/null @@ -1,10 +0,0 @@ -Admin.ApisServerFormView = Ember.View.extend({ - templateName: 'apis/server_form', - - didInsertElement: function() { - var title = this.get('controller.title'); - if(title) { - this.set('controller.controllers.modal.title', title); - } - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/table_view.js deleted file mode 100644 index dadaa4b7d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/apis/table_view.js +++ /dev/null @@ -1,136 +0,0 @@ -Admin.ApisTableView = Ember.View.extend({ - tagName: 'table', - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - init: function() { - this._super(); - - // We're observing the controller, which is a computed property on views. - // Force fetching it so the observers fire: - // http://emberjs.com/guides/object-model/observers/#toc_unconsumed-computed-properties-do-not-trigger-observers - this.get('controller'); - }, - - didInsertElement: function() { - this.set('table', this.$().DataTable({ - serverSide: true, - ajax: '/api-umbrella/v1/apis.json', - pageLength: 50, - rowCallback: function(row, data) { - $(row).data('id', data.id); - }, - order: [[0, 'asc']], - columns: [ - { - data: 'name', - title: 'Name', - defaultContent: '-', - render: _.bind(function(name, type, data) { - if(type === 'display' && name && name !== '-') { - var link = '#/apis/' + data.id + '/edit'; - return '' + _.escape(name) + ''; - } - - return name; - }, this), - }, - { - data: 'frontend_host', - title: 'Host', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'frontend_prefixes', - title: 'Prefixes', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'sort_order', - title: 'Matching Order', - defaultContent: '-', - width: 130, - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: null, - className: 'reorder-handle', - orderable: false, - render: function() { - return ''; - }, - }, - ] - })); - - this.get('table') - .on('search', _.bind(function(event, settings) { - // Disable reordering if the user tries to filter the table by anything - // (otherwise, our reordering logic won't work, since it relies on the - // neighboring rows). - if(this.get('controller.reorderActive')) { - if(settings.oPreviousSearch && settings.oPreviousSearch.sSearch) { - this.set('controller.reorderActive', false); - } - } - }, this)) - .on('order', _.bind(function(event, settings) { - // Disable reordering if the user tries to sort the table by anything - // other than the sort order (otherwise, our reordering logic won't - // work, since it relies on the neighboring rows). - if(this.get('controller.reorderActive')) { - if(settings.aaSorting && !_.isEqual(settings.aaSorting, [[3, 'asc']])) { - this.set('controller.reorderActive', false); - } - } - }, this)); - - this.$().find('tbody').sortable({ - handle: '.reorder-handle', - placeholder: 'reorder-placeholder', - helper: function(event, ui) { - ui.children().each(function() { - $(this).width($(this).width()); - }); - return ui; - }, - stop: _.bind(function(event, ui) { - var row = $(ui.item); - var previousRow = row.prev('tbody tr'); - var moveAfterId = null; - if(previousRow.length > 0) { - moveAfterId = $(previousRow[0]).data('id'); - } - - this.saveReorder(row.data('id'), moveAfterId); - }, this), - }); - }, - - handleReorderChange: function() { - if(this.get('controller.reorderActive')) { - this.$().addClass('reorder-active'); - this.get('table') - .order([[3, 'asc']]) - .search('') - .draw(); - } else { - this.$().removeClass('reorder-active'); - } - }.observes('controller.reorderActive'), - - saveReorder: function(id, moveAfterId) { - this.$().dataTable().fnProcessingIndicator(true); - $.ajax({ - url: '/api-umbrella/v1/apis/' + id + '/move_after.json', - type: 'PUT', - data: { move_after_id: moveAfterId }, - }).done(_.bind(function() { - this.get('table').draw(); - }, this)).fail(_.bind(function() { - bootbox.alert('An unexpected error occurred. Please try again.'); - this.get('table').draw(); - }, this)); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/boolean_radio_button_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/boolean_radio_button_view.js deleted file mode 100644 index 80224b321..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/boolean_radio_button_view.js +++ /dev/null @@ -1,25 +0,0 @@ -Admin.BooleanRadioButtonView = Ember.View.extend({ - tagName: 'input', - type: 'radio', - attributeBindings: ['name', 'type', 'checked:checked:'], - - click: function() { - this.set('selection', true); - - var otherRadios = $('input[name="' + this.$().attr('name') + '"]').not(this.$()); - _.each(otherRadios, function(radio) { - var radioView = Ember.View.views[radio.id]; - if(radioView) { - radioView.deselect(); - } - }); - }, - - deselect: function() { - this.set('selection', false); - }, - - checked: function() { - return !!this.get('selection'); - }.property() -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/checkbox_list_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/checkbox_list_view.js deleted file mode 100644 index 545c00b03..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/checkbox_list_view.js +++ /dev/null @@ -1,29 +0,0 @@ -Admin.CheckboxListItemView = Ember.Checkbox.extend({ - checked: function() { - var checkedValues = this.get('checkedValues') || []; - var value = this.get('content.id'); - return _.contains(checkedValues, value); - }.property('content', 'checkedValues.@each'), - - change: function() { - var checkedValues = this.get('checkedValues') || []; - var value = this.get('content.id'); - - if(this.get('checked')) { - checkedValues.push(value); - } else { - checkedValues = _.without(checkedValues, value); - } - - checkedValues = _.uniq(checkedValues).sort(); - this.set('checkedValues', checkedValues); - } -}); - -Admin.CheckboxListView = Ember.CollectionView.extend({ - itemViewClass: Ember.View.extend({ - checkedValuesBinding: 'parentView.checkedValues', - - template: Ember.Handlebars.compile('') - }), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/config/publish_record_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/config/publish_record_view.js deleted file mode 100644 index 31f41b1a2..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/config/publish_record_view.js +++ /dev/null @@ -1,7 +0,0 @@ -Admin.ConfigPublishRecordView = Ember.View.extend({ - actions: { - toggleConfigDiff: function(id) { - $('[data-diff-id=' + id + ']').toggle(); - } - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/config/publish_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/config/publish_view.js deleted file mode 100644 index 52d40d803..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/config/publish_view.js +++ /dev/null @@ -1,77 +0,0 @@ -Admin.ConfigPublishView = Ember.View.extend({ - didInsertElement: function() { - this.$submitButton = $('#publish_button'); - this.$toggleCheckboxesLink = $('#toggle_checkboxes'); - $('#publish_form').on('change', ':checkbox', _.bind(this.onCheckboxChange, this)); - - var $checkboxes = $('#publish_form :checkbox'); - if($checkboxes.length === 1) { - $checkboxes.prop('checked', true); - } - - this.onCheckboxChange(); - - this.$().find('.diff-active-yaml').each(function() { - var activeYaml = $(this).text(); - var pendingYaml = $(this).siblings('.diff-pending-yaml').text(); - - var diff = JsDiff.diffWords(activeYaml, pendingYaml); - - var fragment = document.createDocumentFragment(); - for(var i = 0; i < diff.length; i++) { - if(diff[i].added && diff[i + 1] && diff[i + 1].removed) { - var swap = diff[i]; - diff[i] = diff[i + 1]; - diff[i + 1] = swap; - } - - var node; - if(diff[i].removed) { - node = document.createElement('del'); - node.appendChild(document.createTextNode(diff[i].value)); - } else if(diff[i].added) { - node = document.createElement('ins'); - node.appendChild(document.createTextNode(diff[i].value)); - } else { - node = document.createTextNode(diff[i].value); - } - - fragment.appendChild(node); - } - - var diffOutput = $(this).siblings('.config-diff'); - diffOutput.html(fragment); - }); - }, - - onCheckboxChange: function() { - var $unchecked = $('#publish_form :checkbox:not(:checked)'); - if($unchecked.length > 0) { - this.$toggleCheckboxesLink.text(this.$toggleCheckboxesLink.data('check-all')); - } else { - this.$toggleCheckboxesLink.text(this.$toggleCheckboxesLink.data('uncheck-all')); - } - - var $checked = $('#publish_form :checkbox:checked'); - if($checked.length > 0) { - this.$submitButton.prop('disabled', false); - } else { - this.$submitButton.prop('disabled', true); - } - }, - - actions: { - toggleAllCheckboxes: function() { - var $checkboxes = $('#publish_form :checkbox'); - var $unchecked = $('#publish_form :checkbox').not(':checked'); - - if($unchecked.length > 0) { - $checkboxes.prop('checked', true); - } else { - $checkboxes.prop('checked', false); - } - - this.onCheckboxChange(); - } - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/hide_modal_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/hide_modal_view.js deleted file mode 100644 index 4234bac7c..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/hide_modal_view.js +++ /dev/null @@ -1,8 +0,0 @@ -Admin.HideModalView = Ember.View.extend({ - render: function() { - }, - - didInsertElement: function() { - $('.modal').modal('hide'); - } -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/loading_overlay_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/loading_overlay_view.js deleted file mode 100644 index 00986cc3f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/loading_overlay_view.js +++ /dev/null @@ -1,55 +0,0 @@ -Admin.LoadingOverlayView = Ember.View.extend({ - classNames: ['loading-overlay'], - attributeBindings: ['style'], - style: 'display: none;', - - init: function() { - this._super(); - - var opts = { - lines: 13, // The number of lines to draw - length: 20, // The length of each line - width: 10, // The line thickness - radius: 30, // The radius of the inner circle - corners: 1, // Corner roundness (0..1) - rotate: 0, // The rotation offset - direction: 1, // 1: clockwise, -1: counterclockwise - color: '#000', // #rgb or #rrggbb - speed: 1, // Rounds per second - trail: 60, // Afterglow percentage - shadow: false, // Whether to render a shadow - hwaccel: true, // Whether to use hardware acceleration - className: 'spinner', // The CSS class to assign to the spinner - zIndex: 2e9, // The z-index (defaults to 2000000000) - }; - - this.spinner = new Spinner(opts); - }, - - didInsertElement: function() { - if(this.get('isLoading')) { - this.showSpinner(); - } - }, - - showSpinner: function() { - this.$().show(); - this.spinner.spin(this.$()[0]); - }, - - hideSpinner: function() { - if(this.spinner) { - this.spinner.stop(); - } - - this.$().hide(); - }, - - toggleSpinner: function() { - if(this.get('isLoading')) { - this.showSpinner(); - } else { - this.hideSpinner(); - } - }.observes('isLoading'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/drilldown_chart_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/drilldown_chart_view.js deleted file mode 100644 index e377ec565..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/drilldown_chart_view.js +++ /dev/null @@ -1,91 +0,0 @@ -Admin.StatsDrilldownChartView = Ember.View.extend({ - data: [], - - chartOptions: { - pointSize: 0, - lineWidth: 1, - focusTarget: 'category', - width: '100%', - chartArea: { - width: '95%', - height: '88%', - top: 0, - }, - fontSize: 12, - isStacked: true, - areaOpacity: 0.2, - vAxis: { - gridlines: { - count: 4 - }, - textStyle: { - fontSize: 11, - }, - textPosition: 'in', - }, - hAxis: { - format: 'MMM d', - baselineColor: 'transparent', - gridlines: { - color: 'transparent', - }, - }, - legend: { - position: 'none', - } - }, - - chartData: { - cols: [ - {id: 'date', label: 'Date', type: 'datetime'}, - {id: 'hits', label: 'Hits', type: 'number'}, - ], - rows: [] - }, - - didInsertElement: function() { - this.chart = new google.visualization.AreaChart(this.$()[0]); - - // On first load, refresh the data. Afterwards the observer should handle - // refreshing. - if(!this.dataTable) { - this.refreshData(); - } - - $(window).on('resize', _.debounce(this.draw.bind(this), 100)); - }, - - refreshData: function() { - this.chartData = this.get('data'); - for(var i = 0; i < this.chartData.rows.length; i++) { - this.chartData.rows[i].c[0].v = new Date(this.chartData.rows[i].c[0].v); - } - - // Show hours on the axis when viewing minutely date. - switch(this.get('controller.query.params.interval')) { - case 'minute': - this.chartOptions.hAxis.format = 'MMM d h a'; - break; - default: - this.chartOptions.hAxis.format = 'MMM d'; - break; - } - - // Show hours on the axis when viewing less than 2 days of hourly data. - if(this.get('controller.query.params.interval') === 'hour') { - var start = moment(this.get('controller.query.params.start_at')); - var end = moment(this.get('controller.query.params.end_at')); - var maxDuration = 2 * 24 * 60 * 60; // 2 days - if(end.unix() - start.unix() <= maxDuration) { - this.chartOptions.hAxis.format = 'MMM d h a'; - } - } - - this.dataTable = new google.visualization.DataTable(this.chartData); - this.draw(); - }.observes('data'), - - draw: function() { - this.chart.draw(this.dataTable, this.chartOptions); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/drilldown_table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/drilldown_table_view.js deleted file mode 100644 index 80b888312..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/drilldown_table_view.js +++ /dev/null @@ -1,54 +0,0 @@ -Admin.StatsDrilldownTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.$().DataTable({ - searching: false, - order: [[1, 'desc']], - data: this.get('model.results'), - columns: [ - { - data: 'path', - title: 'Path', - defaultContent: '-', - render: _.bind(function(name, type, data) { - if(type === 'display' && name && name !== '-') { - if(data.terminal) { - return '' + _.escape(name); - } else { - var link, params; - params = _.clone(this.get('controller.query.params')); - params.prefix = data.descendent_prefix; - link = '#/stats/drilldown/' + $.param(params); - - return '' + _.escape(name) + ''; - } - } - - return name; - }, this), - }, - { - data: 'hits', - title: 'Hits', - defaultContent: '-', - render: function(number, type) { - if(type === 'display' && number && number !== '-') { - return numeral(number).format('0,0'); - } - - return number; - }, - }, - ] - }); - }, - - refreshData: function() { - var table = this.$().DataTable(); - table.clear(); - table.rows.add(this.get('model.results')).draw(); - }.observes('model.results'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/facet_table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/facet_table_view.js deleted file mode 100644 index c1681e142..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/facet_table_view.js +++ /dev/null @@ -1,17 +0,0 @@ -Admin.StatsFacetTableView = Ember.View.extend({ - templateName: 'stats/_facet_table', - - setLinks: function() { - _.each(this.data, _.bind(function(bucket) { - var params = _.clone(this.get('controller.query.params')); - params.search = _.compact([params.search, this.facetTerm + ':"' + bucket.key + '"']).join(' AND '); - bucket.linkQuery = $.param(params); - }, this)); - }.observes('data').on('init'), - - actions: { - toggleFacetTable: function() { - this.$().find('table').toggle(); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/interval_chart_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/interval_chart_view.js deleted file mode 100644 index fe25ebf86..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/interval_chart_view.js +++ /dev/null @@ -1,97 +0,0 @@ -Admin.StatsIntervalChartView = Ember.View.extend({ - data: [], - - chartOptions: { - focusTarget: 'category', - width: '100%', - chartArea: { - width: '95%', - height: '88%', - top: 0, - }, - fontSize: 12, - colors: ['#4682B4'], - areaOpacity: 0.2, - vAxis: { - gridlines: { - count: 4 - }, - textStyle: { - fontSize: 11, - }, - textPosition: 'in', - }, - hAxis: { - format: 'MMM d', - baselineColor: 'transparent', - gridlines: { - color: 'transparent', - }, - }, - legend: { - position: 'none', - } - }, - - chartData: { - cols: [ - {id: 'date', label: 'Date', type: 'datetime'}, - {id: 'hits', label: 'Hits', type: 'number'}, - ], - rows: [] - }, - - didInsertElement: function() { - this.chart = new google.visualization.AreaChart(this.$()[0]); - - // On first load, refresh the data. Afterwards the observer should handle - // refreshing. - if(!this.dataTable) { - this.refreshData(); - } - - $(window).on('resize', _.debounce(this.draw.bind(this), 100)); - }, - - refreshData: function() { - this.chartData.rows = this.get('data') || []; - for(var i = 0; i < this.chartData.rows.length; i++) { - this.chartData.rows[i].c[0].v = new Date(this.chartData.rows[i].c[0].v); - } - - if(this.chartData.rows.length < 100) { - this.chartOptions.pointSize = 8; - this.chartOptions.lineWidth = 4; - } else { - this.chartOptions.pointSize = 0; - this.chartOptions.lineWidth = 3; - } - - // Show hours on the axis when viewing minutely date. - switch(this.get('controller.query.params.interval')) { - case 'minute': - this.chartOptions.hAxis.format = 'MMM d h a'; - break; - default: - this.chartOptions.hAxis.format = 'MMM d'; - break; - } - - // Show hours on the axis when viewing less than 2 days of hourly data. - if(this.get('controller.query.params.interval') === 'hour') { - var start = moment(this.get('controller.query.params.start_at')); - var end = moment(this.get('controller.query.params.end_at')); - var maxDuration = 2 * 24 * 60 * 60; // 2 days - if(end.unix() - start.unix() <= maxDuration) { - this.chartOptions.hAxis.format = 'MMM d h a'; - } - } - - this.dataTable = new google.visualization.DataTable(this.chartData); - this.draw(); - }.observes('data'), - - draw: function() { - this.chart.draw(this.dataTable, this.chartOptions); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/logs_table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/logs_table_view.js deleted file mode 100644 index 83bb648c6..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/logs_table_view.js +++ /dev/null @@ -1,188 +0,0 @@ -Admin.LogsTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.$().DataTable({ - searching: false, - serverSide: true, - ajax: { - url: '/admin/stats/logs.json', - // Use POST for this endpoint, since the URLs can be very long and - // exceed URL length limits in IE (and apparently Capybara too). - type: 'POST', - data: _.bind(function(data) { - var query = this.get('controller.query.params'); - return _.extend({}, data, query); - }, this) - }, - drawCallback: _.bind(function() { - this.$().find('td').truncate({ - width: 400, - addtitle: true, - addclass: 'truncated' - }); - - this.$().find('.truncated').qtip({ - style: { - classes: 'qtip-bootstrap qtip-forced-wide', - }, - hide: { - fixed: true, - delay: 200 - }, - position: { - viewport: false, - my: 'bottom center', - at: 'top center' - } - }); - }, this), - order: [[0, 'desc']], - columns: [ - { - data: 'request_at', - type: 'date', - title: 'Time', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderTime, - }, - { - data: 'request_method', - title: 'Method', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_host', - title: 'Host', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_url', - title: 'URL', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'user_email', - title: 'User', - defaultContent: '-', - render: _.bind(function(email, type, data) { - if(type === 'display' && email && email !== '-') { - var params = _.clone(this.get('controller.query.params')); - params.search = _.compact([params.search, 'user_id:"' + data.user_id + '"']).join(' AND '); - var link = '#/stats/logs/' + $.param(params); - - return '' + _.escape(email) + ''; - } - - return email; - }, this), - }, - { - data: 'request_ip', - title: 'IP Address', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_ip_country', - title: 'Country', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_ip_region', - title: 'State', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_ip_city', - title: 'City', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'response_status', - title: 'Status', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'gatekeeper_denied_code', - title: 'Reason Denied', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'response_time', - title: 'Response Time', - defaultContent: '-', - render: function(time, type) { - if(type === 'display' && time && time !== '-') { - return time + ' ms'; - } - - return time; - }, - }, - { - data: 'response_content_type', - title: 'Content Type', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_accept_encoding', - title: 'Accept Encoding', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_user_agent', - title: 'User Agent', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_user_agent_family', - title: 'User Agent Family', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_user_agent_type', - title: 'User Agent Type', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_referer', - title: 'Referer', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'request_origin', - title: 'Origin', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - ] - }); - }, - - redrawTable: function() { - this.$().DataTable().draw(); - }, - - refreshData: function() { - // Wrap datatables redraw in Ember.run.once so that we only trigger it once - // even if multiple query parameters are being changed at once. - Ember.run.once(this, 'redrawTable'); - }.observes('controller.query.params.query', 'controller.query.params.search', 'controller.query.params.start_at', 'controller.query.params.end_at', 'controller.query.params.beta_analytics'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/logs_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/logs_view.js deleted file mode 100644 index 5920ee5a6..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/logs_view.js +++ /dev/null @@ -1,2 +0,0 @@ -Admin.StatsLogsView = Ember.View.extend({ -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/map_geo_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/map_geo_view.js deleted file mode 100644 index ba412196f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/map_geo_view.js +++ /dev/null @@ -1,81 +0,0 @@ -Admin.StatsMapGeoView = Ember.View.extend({ - data: [], - - chartOptions: { - width: 640, - colorAxis: { - colors: ['#B0DBFF', '#4682B4'], - }, - }, - - chartData: { - cols: [], - rows: [] - }, - - didInsertElement: function() { - this.chart = new google.visualization.GeoChart(this.$()[0]); - google.visualization.events.addListener(this.chart, 'regionClick', _.bind(this.handleRegionClick, this)); - google.visualization.events.addListener(this.chart, 'select', _.bind(this.handleCityClick, this)); - - // On first load, refresh the data. Afterwards the observer should handle - // refreshing. - if(!this.dataTable) { - this.refreshData(); - } - - $(window).on('resize', _.debounce(this.draw.bind(this), 100)); - }, - - handleRegionClick: function(region) { - this.set('controller.query.params.region', region.region); - }, - - handleCityClick: function() { - if(this.get('model.region_field') === 'request_ip_city') { - var selection = this.chart.getSelection(); - if(selection) { - var rowIndex = selection[0].row; - var region = this.dataTable.getValue(rowIndex, 2); - - var params = _.clone(this.get('controller.query.params')); - params.search = 'request_ip_city:"' + region + '"'; - var router = this.get('controller.target.router'); - router.transitionTo('stats.logs', $.param(params)); - } - } - }, - - refreshData: function() { - this.chartData.rows = this.get('model.map_regions') || []; - this.chartData.cols = [ - {id: 'region', label: 'Region', type: 'string'}, - {id: 'startDate', label: 'Hits', type: 'number'}, - ]; - - if(this.get('model.region_field') === 'request_ip_city') { - this.chartData.cols.unshift({id: 'latitude', label: 'Latitude', type: 'number'}, - {id: 'longitude', label: 'Longitude', type: 'number'}); - } - - this.chartOptions.region = this.get('controller.query.params.region'); - if(this.chartOptions.region.indexOf('US') === 0) { - this.chartOptions.resolution = 'provinces'; - } else { - this.chartOptions.resolution = 'countries'; - } - - if(this.chartOptions.region === 'world' || this.chartOptions.region === 'US') { - this.chartOptions.displayMode = 'regions'; - } else { - this.chartOptions.displayMode = 'markers'; - } - - this.dataTable = new google.visualization.DataTable(this.chartData); - this.draw(); - }.observes('model.map_regions'), - - draw: function() { - this.chart.draw(this.dataTable, this.chartOptions); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/map_table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/map_table_view.js deleted file mode 100644 index 11007e487..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/map_table_view.js +++ /dev/null @@ -1,56 +0,0 @@ -Admin.StatsMapTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.$().DataTable({ - searching: false, - order: [[1, 'desc']], - data: this.get('model.regions'), - columns: [ - { - data: 'name', - title: 'Location', - defaultContent: '-', - render: _.bind(function(name, type, data) { - if(type === 'display' && name && name !== '-') { - var link, params; - if(this.get('model.region_field') === 'request_ip_city') { - params = _.clone(this.get('controller.query.params')); - params.search = 'request_ip_city:"' + data.id + '"'; - link = '#/stats/logs/' + $.param(params); - } else { - params = _.clone(this.get('controller.query.params')); - params.region = data.id; - link = '#/stats/map/' + $.param(params); - } - - return '' + _.escape(name) + ''; - } - - return name; - }, this), - }, - { - data: 'hits', - title: 'Hits', - defaultContent: '-', - render: function(number, type) { - if(type === 'display' && number && number !== '-') { - return numeral(number).format('0,0'); - } - - return number; - }, - }, - ] - }); - }, - - refreshData: function() { - var table = this.$().DataTable(); - table.clear(); - table.rows.add(this.get('model.regions')).draw(); - }.observes('model.regions'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/query_form_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/query_form_view.js deleted file mode 100644 index 9eb496caa..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/query_form_view.js +++ /dev/null @@ -1,348 +0,0 @@ -Admin.StatsQueryFormView = Ember.View.extend({ - templateName: 'stats/_query_form', - - enableInterval: false, - - enableBetaAnalytics: enableBetaAnalytics, - - datePickerRanges: { - 'Today': [ - moment().startOf('day'), - moment().endOf('day'), - ], - 'Yesterday': [ - moment().subtract(1, 'days'), - moment().subtract(1, 'days').endOf('day'), - ], - 'Last 7 Days': [ - moment().subtract(6, 'days'), - moment().endOf('day'), - ], - 'Last 30 Days': [ - moment().subtract(29, 'days').startOf('day'), - moment().endOf('day'), - ], - 'This Month': [ - moment().startOf('month'), - moment().endOf('month'), - ], - 'Last Month': [ - moment().subtract(1, 'month').startOf('month'), - moment().subtract(1, 'month').endOf('month'), - ] - }, - - didInsertElement: function() { - this.updateInterval(); - this.updateDateRange(); - this.updateBetaAnalytics(); - - $('#reportrange').daterangepicker({ - ranges: this.datePickerRanges, - startDate: moment(this.get('controller.query.params.start_at'), 'YYYY-MM-DD'), - endDate: moment(this.get('controller.query.params.end_at'), 'YYYY-MM-DD'), - }, _.bind(this.handleDateRangeChange, this)); - - var stringOperators = [ - 'begins_with', - 'not_begins_with', - 'equal', - 'not_equal', - 'contains', - 'not_contains', - 'is_null', - 'is_not_null', - ]; - - var selectOperators = [ - 'equal', - 'not_equal', - 'is_null', - 'is_not_null', - ]; - - var numberOperators = [ - 'equal', - 'not_equal', - 'less', - 'less_or_equal', - 'greater', - 'greater_or_equal', - 'between', - 'is_null', - 'is_not_null', - ]; - - var $queryBuilder = $('#query_builder').queryBuilder({ - plugins: { - 'filter-description': { - icon: 'fa fa-info-circle', - mode: 'bootbox', - }, - 'bt-tooltip-errors': null - }, - allow_empty: true, - allow_groups: false, - filters: [ - { - id: 'request_method', - label: polyglot.t('admin.stats.fields.request_method.label'), - description: polyglot.t('admin.stats.fields.request_method.description_markdown'), - type: 'string', - operators: selectOperators, - input: 'select', - values: { - 'get': 'GET', - 'post': 'POST', - 'put': 'PUT', - 'delete': 'DELETE', - 'head': 'HEAD', - 'patch': 'PATCH', - 'options': 'OPTIONS', - }, - }, - { - id: 'request_scheme', - label: polyglot.t('admin.stats.fields.request_scheme.label'), - description: polyglot.t('admin.stats.fields.request_scheme.description_markdown'), - type: 'string', - operators: selectOperators, - input: 'select', - values: { - 'http': 'http', - 'https': 'https', - }, - }, - { - id: 'request_host', - label: polyglot.t('admin.stats.fields.request_host.label'), - description: polyglot.t('admin.stats.fields.request_host.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_path', - label: polyglot.t('admin.stats.fields.request_path.label'), - description: polyglot.t('admin.stats.fields.request_path.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_url', - label: polyglot.t('admin.stats.fields.request_url.label'), - description: polyglot.t('admin.stats.fields.request_url.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_ip', - label: polyglot.t('admin.stats.fields.request_ip.label'), - description: polyglot.t('admin.stats.fields.request_ip.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_ip_country', - label: polyglot.t('admin.stats.fields.request_ip_country.label'), - description: polyglot.t('admin.stats.fields.request_ip_country.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_ip_region', - label: polyglot.t('admin.stats.fields.request_ip_region.label'), - description: polyglot.t('admin.stats.fields.request_ip_region.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_ip_city', - label: polyglot.t('admin.stats.fields.request_ip_city.label'), - description: polyglot.t('admin.stats.fields.request_ip_city.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_user_agent', - label: polyglot.t('admin.stats.fields.request_user_agent.label'), - description: polyglot.t('admin.stats.fields.request_user_agent.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_user_agent_family', - label: polyglot.t('admin.stats.fields.request_user_agent_family.label'), - description: polyglot.t('admin.stats.fields.request_user_agent_family.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_user_agent_type', - label: polyglot.t('admin.stats.fields.request_user_agent_type.label'), - description: polyglot.t('admin.stats.fields.request_user_agent_type.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_referer', - label: polyglot.t('admin.stats.fields.request_referer.label'), - description: polyglot.t('admin.stats.fields.request_referer.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'request_origin', - label: polyglot.t('admin.stats.fields.request_origin.label'), - description: polyglot.t('admin.stats.fields.request_origin.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'api_key', - label: polyglot.t('admin.stats.fields.api_key.label'), - description: polyglot.t('admin.stats.fields.api_key.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'user_email', - label: polyglot.t('admin.stats.fields.user_email.label'), - description: polyglot.t('admin.stats.fields.user_email.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'user_id', - label: polyglot.t('admin.stats.fields.user_id.label'), - description: polyglot.t('admin.stats.fields.user_id.description_markdown'), - type: 'string', - operators: stringOperators, - }, - { - id: 'response_status', - label: polyglot.t('admin.stats.fields.response_status.label'), - description: polyglot.t('admin.stats.fields.response_status.description_markdown'), - type: 'integer', - operators: numberOperators, - }, - { - id: 'gatekeeper_denied_code', - label: polyglot.t('admin.stats.fields.gatekeeper_denied_code.label'), - description: polyglot.t('admin.stats.fields.gatekeeper_denied_code.description_markdown'), - type: 'string', - operators: selectOperators, - input: 'select', - values: { - 'not_found': 'not_found', - 'api_key_missing': 'api_key_missing', - 'api_key_invalid': 'api_key_invalid', - 'api_key_disabled': 'api_key_disabled', - 'api_key_unverified': 'api_key_unverified', - 'api_key_unauthorized': 'api_key_unauthorized', - 'over_rate_limit': 'over_rate_limit', - 'internal_server_error': 'internal_server_error', - 'https_required': 'https_required', - }, - }, - { - id: 'response_time', - label: polyglot.t('admin.stats.fields.response_time.label'), - description: polyglot.t('admin.stats.fields.response_time.description_markdown'), - type: 'integer', - operators: numberOperators, - }, - { - id: 'response_content_type', - label: polyglot.t('admin.stats.fields.response_content_type.label'), - description: polyglot.t('admin.stats.fields.response_content_type.description_markdown'), - type: 'string', - operators: stringOperators, - }, - ], - }); - - var query = this.get('controller.query.params.query'); - var rules; - if(query) { - rules = JSON.parse(query); - } - - if(rules) { - if(rules.condition) { - $queryBuilder.queryBuilder('setRules', rules); - } - - this.send('toggleFilters'); - this.send('toggleFilterType', 'builder'); - } else if(this.get('controller.query.params.search')) { - this.send('toggleFilters'); - this.send('toggleFilterType', 'advanced'); - } - }, - - updateQueryBuilderRules: function() { - var query = this.get('controller.query.params.query'); - var rules; - if(query) { - rules = JSON.parse(query); - } - - if(rules && rules.condition) { - $('#query_builder').queryBuilder('setRules', rules); - } else { - $('#query_builder').queryBuilder('reset'); - } - }.observes('controller.query.params.query'), - - updateInterval: function() { - var interval = this.get('controller.query.params.interval'); - $('#interval_buttons').find('button[value="' + interval + '"]').button('toggle'); - }.observes('controller.query.params.interval'), - - updateDateRange: function() { - var start = moment(this.get('controller.query.params.start_at')); - var end = moment(this.get('controller.query.params.end_at')); - - $('#reportrange span.text').html(start.format('MMM D, YYYY') + ' - ' + end.format('MMM D, YYYY')); - }.observes('controller.query.params.start_at', 'controller.query.params.end_at'), - - handleDateRangeChange: function(start, end) { - this.setProperties({ - 'controller.query.params.start_at': start.format('YYYY-MM-DD'), - 'controller.query.params.end_at': end.format('YYYY-MM-DD'), - }); - }, - - updateBetaAnalytics: function() { - this.set('isBetaAnalytics', this.get('controller.query.params.beta_analytics') === 'true'); - }.observes('controller.query.params.beta_analytics'), - - handleBetaAnalytics: function() { - this.set('controller.query.params.beta_analytics', this.get('isBetaAnalytics').toString()); - }.observes('isBetaAnalytics'), - - actions: { - toggleFilters: function() { - var $container = $('#filters_ui'); - var $icon = $('#filter_toggle .fa'); - if($container.is(':visible')) { - $icon.addClass('fa-caret-right'); - $icon.removeClass('fa-caret-down'); - } else { - $icon.addClass('fa-caret-down'); - $icon.removeClass('fa-caret-right'); - } - - $container.slideToggle(100); - }, - - toggleFilterType: function(type) { - $('.filter-type').hide(); - $('#filter_type_' + type).show(); - }, - - clickInterval: function(interval) { - this.set('controller.query.params.interval', interval); - }, - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/users_table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/users_table_view.js deleted file mode 100644 index 29a60efa7..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/stats/users_table_view.js +++ /dev/null @@ -1,86 +0,0 @@ -Admin.StatsUsersTableView = Ember.View.extend({ - tagName: 'table', - - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.$().DataTable({ - searching: false, - serverSide: true, - ajax: { - url: '/admin/stats/users.json', - data: _.bind(function(data) { - var query = this.get('controller.query.params'); - return _.extend({}, data, query); - }, this) - }, - order: [[4, 'desc']], - columns: [ - { - data: 'email', - title: 'Email', - defaultContent: '-', - render: _.bind(function(email, type, data) { - if(type === 'display' && email && email !== '-') { - var params = _.clone(this.get('controller.query.params')); - params.search = 'user_id:"' + data.id + '"'; - var link = '#/stats/logs/' + $.param(params); - - return '' + _.escape(email) + ''; - } - - return email; - }, this), - }, - { - data: 'first_name', - title: 'First Name', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'last_name', - title: 'Last Name', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - { - data: 'created_at', - type: 'date', - title: 'Signed Up', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderTime, - }, - { - data: 'hits', - title: 'Hits', - defaultContent: '-', - render: function(number, type) { - if(type === 'display' && number && number !== '-') { - return numeral(number).format('0,0'); - } - - return number; - }, - }, - { - data: 'last_request_at', - type: 'date', - title: 'Last Request', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderTime, - }, - { - data: 'use_description', - title: 'Use Description', - defaultContent: '-', - render: Admin.DataTablesHelpers.renderEscaped, - }, - ] - }); - }, - - refreshData: function() { - this.$().DataTable().draw(); - }.observes('controller.query.params.query', 'controller.query.params.search', 'controller.query.params.start_at', 'controller.query.params.end_at', 'controller.query.params.beta_analytics'), -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/website_backends/table_view.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/views/website_backends/table_view.js deleted file mode 100644 index d4717d9eb..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/views/website_backends/table_view.js +++ /dev/null @@ -1,31 +0,0 @@ -Admin.WebsiteBackendsTableView = Ember.View.extend({ - tagName: 'table', - classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'], - - didInsertElement: function() { - this.set('table', this.$().DataTable({ - serverSide: true, - ajax: '/api-umbrella/v1/website_backends.json', - pageLength: 50, - rowCallback: function(row, data) { - $(row).data('id', data.id); - }, - order: [[0, 'asc']], - columns: [ - { - data: 'frontend_host', - title: 'Host', - defaultContent: '-', - render: _.bind(function(name, type, data) { - if(type === 'display' && name && name !== '-') { - var link = '#/website_backends/' + data.id + '/edit'; - return '' + _.escape(name) + ''; - } - - return name; - }, this), - }, - ] - })); - }, -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin_test.js b/src/api-umbrella/web-app/app/assets/javascripts/admin_test.js deleted file mode 100644 index ef37f8706..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin_test.js +++ /dev/null @@ -1,15 +0,0 @@ -//= require jquery-simulate-ext/libs/jquery.simulate.js -//= require jquery-simulate-ext/src/jquery.simulate.ext.js -//= require jquery-simulate-ext/src/jquery.simulate.drag-n-drop.js - -// Attempt to disable animations in test mode to improve the -// reliability of some Capybara timing stuff without sleeping: -// http://stackoverflow.com/a/13119950 -// The other part of this is altering the CSS to disable transitions in -// admin_test.css. -$.support.transition = false; -$.fx.off = true; -$(document).ready(function() { - $.support.transition = false; - $.fx.off = true; -}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/application.js b/src/api-umbrella/web-app/app/assets/javascripts/application.js deleted file mode 100644 index da88eb289..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/application.js +++ /dev/null @@ -1 +0,0 @@ -//= require jquery/jquery.js diff --git a/src/api-umbrella/web-app/app/assets/javascripts/html5.js b/src/api-umbrella/web-app/app/assets/javascripts/html5.js deleted file mode 100644 index fc8384514..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/html5.js +++ /dev/null @@ -1,4 +0,0 @@ -// iepp v2.1pre @jon_neal & @aFarkas github.com/aFarkas/iepp -// html5shiv @rem remysharp.com/html5-enabling-script -// Dual licensed under the MIT or GPL Version 2 licenses -/*@cc_on(function(a,b){function r(a){var b=-1;while(++b<'span6'f>r>t<'row-fluid'<'span6'i><'span6'p>>", - "sPaginationType": "bootstrap", - "oLanguage": { - "sLengthMenu": "_MENU_ records per page" - } -} ); - - -/* Default class modification */ -$.extend( $.fn.dataTableExt.oStdClasses, { - "sWrapper": "dataTables_wrapper form-inline" -} ); - - -/* API method to get paging information */ -$.fn.dataTableExt.oApi.fnPagingInfo = function ( oSettings ) -{ - return { - "iStart": oSettings._iDisplayStart, - "iEnd": oSettings.fnDisplayEnd(), - "iLength": oSettings._iDisplayLength, - "iTotal": oSettings.fnRecordsTotal(), - "iFilteredTotal": oSettings.fnRecordsDisplay(), - "iPage": oSettings._iDisplayLength === -1 ? - 0 : Math.ceil( oSettings._iDisplayStart / oSettings._iDisplayLength ), - "iTotalPages": oSettings._iDisplayLength === -1 ? - 0 : Math.ceil( oSettings.fnRecordsDisplay() / oSettings._iDisplayLength ) - }; -}; - - -/* Bootstrap style pagination control */ -$.extend( $.fn.dataTableExt.oPagination, { - "bootstrap": { - "fnInit": function( oSettings, nPaging, fnDraw ) { - var oLang = oSettings.oLanguage.oPaginate; - var fnClickHandler = function ( e ) { - e.preventDefault(); - if ( oSettings.oApi._fnPageChange(oSettings, e.data.action) ) { - fnDraw( oSettings ); - } - }; - - $(nPaging).addClass('pagination').append( - '' - ); - var els = $('a', nPaging); - $(els[0]).bind( 'click.DT', { action: "previous" }, fnClickHandler ); - $(els[1]).bind( 'click.DT', { action: "next" }, fnClickHandler ); - }, - - "fnUpdate": function ( oSettings, fnDraw ) { - var iListLength = 5; - var oPaging = oSettings.oInstance.fnPagingInfo(); - var an = oSettings.aanFeatures.p; - var i, ien, j, sClass, iStart, iEnd, iHalf=Math.floor(iListLength/2); - - if ( oPaging.iTotalPages < iListLength) { - iStart = 1; - iEnd = oPaging.iTotalPages; - } - else if ( oPaging.iPage <= iHalf ) { - iStart = 1; - iEnd = iListLength; - } else if ( oPaging.iPage >= (oPaging.iTotalPages-iHalf) ) { - iStart = oPaging.iTotalPages - iListLength + 1; - iEnd = oPaging.iTotalPages; - } else { - iStart = oPaging.iPage - iHalf + 1; - iEnd = iStart + iListLength - 1; - } - - for ( i=0, ien=an.length ; i'+j+'') - .insertBefore( $('li:last', an[i])[0] ) - .bind('click', function (e) { - e.preventDefault(); - oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength; - fnDraw( oSettings ); - } ); - } - - // Add / remove disabled classes from the static elements - if ( oPaging.iPage === 0 ) { - $('li:first', an[i]).addClass('disabled'); - } else { - $('li:first', an[i]).removeClass('disabled'); - } - - if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) { - $('li:last', an[i]).addClass('disabled'); - } else { - $('li:last', an[i]).removeClass('disabled'); - } - } - } - } -} ); - - -/* - * TableTools Bootstrap compatibility - * Required TableTools 2.1+ - */ -if ( $.fn.DataTable.TableTools ) { - // Set the classes that TableTools uses to something suitable for Bootstrap - $.extend( true, $.fn.DataTable.TableTools.classes, { - "container": "DTTT btn-group", - "buttons": { - "normal": "btn", - "disabled": "disabled" - }, - "collection": { - "container": "DTTT_dropdown dropdown-menu", - "buttons": { - "normal": "", - "disabled": "disabled" - } - }, - "print": { - "info": "DTTT_print_info modal" - }, - "select": { - "row": "active" - } - } ); - - // Have the collection use a bootstrap compatible dropdown - $.extend( true, $.fn.DataTable.TableTools.DEFAULTS.oTags, { - "collection": { - "container": "ul", - "button": "li", - "liner": "a" - } - } ); -} - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/LICENSE b/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/LICENSE deleted file mode 100644 index 9e3a87cde..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2011 by Mal Curtis - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/helpers/ckeditor.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/helpers/ckeditor.js deleted file mode 100644 index b8a9f18f2..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/helpers/ckeditor.js +++ /dev/null @@ -1,36 +0,0 @@ -// CkEditor helper, checks to see if CkEditor editors in the given form are dirty - -(function($){ - var ckeditor = { - ignoreAnchorSelector: '.cke_dialog_ui_button, .cke_tpl_list a', - isDirty: function(form){ - editors = ckeditors(form); - $.DirtyForms.dirtylog('Checking ' + editors.length + ' ckeditors for dirtyness.'); - var dirty = 0; - editors.each(function() { if (this.checkDirty()) dirty += 1 }); - $.DirtyForms.dirtylog('There were ' + dirty + ' dirty ckeditors.'); - return dirty > 0; - }, - setClean: function(form){ - ckeditors(form).each(function() { this.resetDirty() }); - } - } - var ckeditors = function(form) { - editors = new Array(); - try { - for (var key in CKEDITOR.instances) { - if (CKEDITOR.instances.hasOwnProperty(key)) { - editor = CKEDITOR.instances[key]; - if ($(editor.element.$).parents().index($(form)) != -1) - editors.push(editor) - } - } - } - catch(e) { - // Ignore, means there was no CKEDITOR variable - } - return $(editors); - } - $.DirtyForms.helpers.push(ckeditor); -})(jQuery); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/jquery.dirtyforms.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/jquery.dirtyforms.js deleted file mode 100644 index b79f49bd4..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/dirtyforms/jquery.dirtyforms.js +++ /dev/null @@ -1,509 +0,0 @@ -/*! - Copyright 2010 Mal Curtis -*/ - -if (typeof jQuery == 'undefined') throw ("jQuery Required"); - -(function($){ - // Public General Plugin methods $.DirtyForms - $.extend({ - DirtyForms: { - debug : false, - message : 'You\'ve made changes on this page which aren\'t saved. If you leave you will lose these changes.', - title : 'Are you sure you want to do that?', - dirtyClass : 'dirty', - listeningClass : 'dirtylisten', - ignoreClass : 'ignoredirty', - choiceContinue : false, - helpers : [], - dialog : { - refire : function(content, ev){ - $.facebox(content); - }, - fire : function(message, title){ - var content = '

' + title + '

' + message + '

ContinueStop'; - $.facebox(content); - }, - bind : function(){ - var close = function(decision) { - return function(e) { - e.preventDefault(); - $(document).trigger('close.facebox'); - decision(e); - }; - }; - $('#facebox .cancel, #facebox .close, #facebox_overlay').click(close(decidingCancel)); - $('#facebox .continue').click(close(decidingContinue)); - }, - stash : function(){ - var fb = $('#facebox'); - return ($.trim(fb.html()) == '' || fb.css('display') != 'block') ? - false : - $('#facebox .content').clone(true); - }, - selector : '#facebox .content' - }, - - isDirty : function(){ - return $(':dirtylistening').dirtyForms('isDirty'); - }, - - disable : function(){ - settings.disabled = true; - }, - - ignoreParentDocs : function(){ - settings.watchParentDocs = false; - }, - - choiceCommit : function(e){ - choiceCommit(e); - }, - - isDeciding : function(){ - return settings.deciding; - }, - - decidingContinue : function(e){ - decidingContinue(e); - }, - - decidingCancel : function(e){ - decidingCancel(e); - }, - - dirtylog : function(msg){ - dirtylog(msg); - } - } - }); - - // Create a custom selector $('form:dirty') - $.extend($.expr[":"], { - dirtylistening : function(a){ - return $(a).hasClass($.DirtyForms.listeningClass); - }, - dirty : function(a){ - return $(a).hasClass($.DirtyForms.dirtyClass); - } - }); - - // Public Element methods ( $('form').dirtyForms('methodName', args) ) - var methods = { - init : function() { - var core = $.DirtyForms; - - dirtylog('Adding forms to watch'); - bindExit(); - - return this.each(function(e){ - if (! $(this).is('form')) return; - dirtylog('Adding form ' + $(this).attr('id') + ' to forms to watch'); - $(this).addClass(core.listeningClass); - - // exclude all HTML 4 except text and password, but include HTML 5 except search - var inputSelector = "textarea,input:not([type='checkbox'],[type='radio'],[type='button']," + - "[type='image'],[type='submit'],[type='reset'],[type='file'],[type='search'])"; - var selectionSelector = "input[type='checkbox'],input[type='radio'],select"; - var resetSelector = "input[type='reset']"; - - // For jQuery 1.7+, use on() - if (typeof $(document).on === 'function') { - $(this).on('focus change',inputSelector,onFocus); - $(this).on('change',selectionSelector,onSelectionChange); - $(this).on('click',resetSelector,onReset); - } else { // For jQuery 1.4.2 - 1.7, use delegate() - $(this).delegate(inputSelector,'focus change',onFocus); - $(this).delegate(selectionSelector,'change',onSelectionChange); - $(this).delegate(resetSelector,'click',onReset); - } - }); - }, - // Returns true if any of the supplied elements are dirty - isDirty : function() { - var isDirty = false; - var node = this; - if (settings.disabled) return false; - if (focusedIsDirty()) { - isDirty = true; - return true; - } - this.each(function(e){ - if($(this).hasClass($.DirtyForms.dirtyClass)){ - isDirty = true; - return true; - } - }); - $.each($.DirtyForms.helpers, function(key,obj){ - if("isDirty" in obj){ - if(obj.isDirty(node)){ - isDirty = true; - return true; - } - } - // For backward compatibility, we call isNodeDirty (deprecated) - if("isNodeDirty" in obj){ - if(obj.isNodeDirty(node)){ - isDirty = true; - return true; - } - } - }); - - dirtylog('isDirty returned ' + isDirty); - return isDirty; - }, - // Marks the element(s) that match the selector dirty - setDirty : function() { - dirtylog('setDirty called'); - return this.each(function(e){ - $(this).addClass($.DirtyForms.dirtyClass).parents('form').addClass($.DirtyForms.dirtyClass); - }); - }, - // "Cleans" this dirty form by essentially forgetting that it is dirty - setClean : function() { - dirtylog('setClean called'); - settings.focused = {element: false, value: false}; - - return this.each(function(e){ - var node = this; - - // remove the current dirty class - $(node).removeClass($.DirtyForms.dirtyClass) - - if ($(node).is('form')) { - // remove all dirty classes from children - $(node).find(':dirty').removeClass($.DirtyForms.dirtyClass); - } else { - // if this is last dirty child, set form clean - var $form = $(node).parents('form'); - if ($form.find(':dirty').length == 0) { - $form.removeClass($.DirtyForms.dirtyClass); - } - } - - // Clean helpers - $.each($.DirtyForms.helpers, function(key,obj){ - if("setClean" in obj){ - obj.setClean(node); - } - }); - }); - } - - // ADD NEW METHODS HERE - }; - - $.fn.dirtyForms = function(method) { - // Method calling logic - if ( methods[method] ) { - return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); - } else if ( typeof method === 'object' || ! method ) { - return methods.init.apply( this, arguments ); - } else { - $.error( 'Method ' + method + ' does not exist on jQuery.dirtyForms' ); - } - }; - - // Deprecated Methods for Backward Compatibility - // DO NOT ADD MORE METHODS LIKE THESE, ADD METHODS WHERE INDICATED ABOVE - $.fn.setDirty = function(){ - return this.dirtyForms('setDirty'); - } - $.fn.isDirty = function(){ - return this.dirtyForms('isDirty'); - } - $.fn.cleanDirty = function(){ - return this.dirtyForms('setClean'); - } - - // Private Properties and Methods - var settings = $.DirtyForms = $.extend({ - watchParentDocs: true, - disabled : false, - exitBound : false, - formStash : false, - dialogStash : false, - deciding : false, - decidingEvent : false, - currentForm : false, - hasFirebug : "console" in window && "firebug" in window.console, - hasConsoleLog: "console" in window && "log" in window.console, - focused: {"element": false, "value": false} - }, $.DirtyForms); - - var onReset = function() { - $(this).parents('form').dirtyForms('setClean'); - } - - var onSelectionChange = function() { - $(this).dirtyForms('setDirty'); - } - - var onFocus = function() { - element = $(this); - if (focusedIsDirty()) { - settings.focused['element'].dirtyForms('setDirty'); - } - settings.focused['element'] = element; - settings.focused['value'] = element.val(); - } - var focusedIsDirty = function() { - // Check, whether the value of focused element has changed - return settings.focused["element"] && - (settings.focused["element"].val() !== settings.focused["value"]); - } - - var dirtylog = function(msg){ - if(!$.DirtyForms.debug) return; - msg = "[DirtyForms] " + msg; - settings.hasFirebug ? - console.log(msg) : - settings.hasConsoleLog ? - window.console.log(msg) : - alert(msg); - } - - var bindExit = function(){ - if(settings.exitBound) return; - - var inIframe = (top !== self); - - // For jQuery 1.7+, use on() - if (typeof $(document).on === 'function') { - $(document).on('click','a',aBindFn); - $(document).on('submit','form',formBindFn); - if (settings.watchParentDocs && inIframe) { - $(top.document).on('click','a',aBindFn); - $(top.document).on('submit','form',formBindFn); - } - } else { // For jQuery 1.4.2 - 1.7, use delegate() - $(document).delegate('a','click',aBindFn); - $(document).delegate('form','submit',formBindFn); - if (settings.watchParentDocs && inIframe) { - $(top.document).delegate('a','click',aBindFn); - $(top.document).delegate('form','submit',formBindFn); - } - } - - $(window).bind('beforeunload', beforeunloadBindFn); - if (settings.watchParentDocs && inIframe) { - $(top.window).bind('beforeunload', beforeunloadBindFn); - } - - settings.exitBound = true; - } - - var getIgnoreAnchorSelector = function(){ - var result = ''; - $.each($.DirtyForms.helpers, function(key,obj){ - if("ignoreAnchorSelector" in obj){ - if (result.length > 0) { result += ','; } - result += obj.ignoreAnchorSelector; - } - }); - return result; - } - - var aBindFn = function(ev){ - // Filter out any anchors the helpers wish to exclude - if (!$(this).is(getIgnoreAnchorSelector())) { - bindFn(ev); - } - } - - var formBindFn = function(ev){ - settings.currentForm = this; - bindFn(ev); - } - - var beforeunloadBindFn = function(ev){ - var result = bindFn(ev); - - if(result && settings.doubleunloadfix != true){ - dirtylog('Before unload will be called, resetting'); - settings.deciding = false; - } - - settings.doubleunloadfix = true; - setTimeout(function(){settings.doubleunloadfix = false;},200); - - // Bug Fix: Only return the result if it is a string, - // otherwise don't return anything. - if (typeof(result) == 'string'){ - ev = ev || window.event; - - // For IE and Firefox prior to version 4 - if (ev) { - ev.returnValue = result; - } - - // For Safari - return result; - } - } - - var bindFn = function(ev){ - dirtylog('Entering: Leaving Event fired, type: ' + ev.type + ', element: ' + ev.target + ', class: ' + $(ev.target).attr('class') + ' and id: ' + ev.target.id); - - if(ev.type == 'beforeunload' && settings.doubleunloadfix){ - dirtylog('Skip this unload, Firefox bug triggers the unload event multiple times'); - settings.doubleunloadfix = false; - return false; - } - - if($(ev.target).hasClass(settings.ignoreClass) || isDifferentTarget(ev)){ - dirtylog('Leaving: Element has ignore class or has target=\'_blank\''); - if(!ev.isDefaultPrevented()){ - clearUnload(); - } - return false; - } - - if(settings.deciding){ - dirtylog('Leaving: Already in the deciding process'); - return false; - } - - if(ev.isDefaultPrevented()){ - dirtylog('Leaving: Event has been stopped elsewhere'); - return false; - } - - if(!settings.isDirty()){ - dirtylog('Leaving: Not dirty'); - if(!ev.isDefaultPrevented()){ - clearUnload(); - } - return false; - } - - if(ev.type == 'submit' && $(ev.target).dirtyForms('isDirty')){ - dirtylog('Leaving: Form submitted is a dirty form'); - if(!ev.isDefaultPrevented()){ - clearUnload(); - } - return true; - } - - settings.deciding = true; - settings.decidingEvent = ev; - dirtylog('Setting deciding active'); - - if(settings.dialog !== false) - { - dirtylog('Saving dialog content'); - settings.dialogStash =settings.dialog.stash(); - dirtylog(settings.dialogStash); - } - - // Callback for page access in current state - $(document).trigger('defer.dirtyforms'); - - if(ev.type == 'beforeunload'){ - //clearUnload(); - dirtylog('Returning to beforeunload browser handler with: ' + settings.message); - return settings.message; - } - if(!settings.dialog) return; - - ev.preventDefault(); - ev.stopImmediatePropagation(); - - if($(ev.target).is('form') && $(ev.target).parents(settings.dialog.selector).length > 0){ - dirtylog('Stashing form'); - settings.formStash = $(ev.target).clone(true).hide(); - }else{ - settings.formStash = false; - } - - dirtylog('Deferring to the dialog'); - settings.dialog.fire($.DirtyForms.message, $.DirtyForms.title); - settings.dialog.bind(); - } - - var isDifferentTarget = function(ev){ - var aTarget = $(ev.target).attr('target'); - if (typeof aTarget === 'string') { - aTarget = aTarget.toLowerCase(); - } - return (aTarget === '_blank'); - } - - var choiceCommit = function(ev){ - if (settings.deciding) { - $(document).trigger('choicecommit.dirtyforms'); - if ($.DirtyForms.choiceContinue) { - decidingContinue(ev); - } else { - decidingCancel(ev); - } - $(document).trigger('choicecommitAfter.dirtyforms'); - } - } - - var decidingCancel = function(ev){ - ev.preventDefault(); - $(document).trigger('decidingcancelled.dirtyforms'); - if(settings.dialog !== false && settings.dialogStash !== false) - { - dirtylog('Refiring the dialog with stashed content'); - settings.dialog.refire(settings.dialogStash.html(), ev); - } - $(document).trigger('decidingcancelledAfter.dirtyforms'); - settings.dialogStash = false; - settings.deciding = settings.currentForm = settings.decidingEvent = false; - } - - var decidingContinue = function(ev){ - window.onbeforeunload = null; // fix for chrome - ev.preventDefault(); - settings.dialogStash = false; - $(document).trigger('decidingcontinued.dirtyforms'); - refire(settings.decidingEvent); - settings.deciding = settings.currentForm = settings.decidingEvent = false; - } - - var clearUnload = function(){ - // I'd like to just be able to unbind this but there seems - // to be a bug in jQuery which doesn't unbind onbeforeunload - dirtylog('Clearing the beforeunload event'); - $(window).unbind('beforeunload', beforeunloadBindFn); - window.onbeforeunload = null; - $(document).trigger('beforeunload.dirtyforms'); - } - - var refire = function(e){ - $(document).trigger('beforeRefire.dirtyforms'); - $(document).trigger('beforeunload.dirtyforms'); - switch(e.type){ - case 'click': - dirtylog("Refiring click event"); - var event = new jQuery.Event('click'); - $(e.target).trigger(event); - if(!event.isDefaultPrevented()){ - var anchor = $(e.target).closest('[href]'); - dirtylog('Sending location to ' + anchor.attr('href')); - location.href = anchor.attr('href'); - return; - } - break; - default: - dirtylog("Refiring " + e.type + " event on " + e.target); - var target; - if(settings.formStash){ - dirtylog('Appending stashed form to body'); - target = settings.formStash; - $('body').append(target); - } - else{ - target = $(e.target); - if(!target.is('form')) - target = target.closest('form'); - } - target.trigger(e.type); - break; - } - } - -})(jQuery); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-easyForm.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-easyForm.js deleted file mode 100644 index c10794e0d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-easyForm.js +++ /dev/null @@ -1,687 +0,0 @@ -// ========================================================================== -// Project: Ember EasyForm -// Copyright: Copyright 2013 DockYard, LLC. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== - - - // Version: 1.0.0.beta.1 - -(function() { -Ember.EasyForm = Ember.Namespace.create({ - VERSION: '1.0.0.beta.1' -}); - -})(); - - - -(function() { -Ember.EasyForm.Config = Ember.Namespace.create({ - _wrappers: { - 'default': { - formClass: '', - fieldErrorClass: 'fieldWithErrors', - inputClass: 'input', - errorClass: 'error', - hintClass: 'hint', - labelClass: '', - wrapControls: false, - controlsWrapperClass: '' - } - }, - _inputTypes: {}, - registerWrapper: function(name, wrapper) { - this._wrappers[name] = Ember.$.extend({}, this._wrappers['default'], wrapper); - }, - getWrapper: function(name) { - var wrapper = this._wrappers[name]; - Ember.assert("The wrapper '" + name + "' was not registered.", wrapper); - return wrapper; - }, - registerInputType: function(name, type){ - this._inputTypes[name] = type; - }, - getInputType: function(name) { - return this._inputTypes[name]; - } -}); - -})(); - - - -(function() { -Ember.Handlebars.registerHelper('error-field', function(property, options) { - options = Ember.EasyForm.processOptions(property, options); - - if (options.hash.propertyBinding) { - options.hash.property = Ember.Handlebars.get(this, options.hash.propertyBinding, options); - } - return Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Error, options); -}); - -})(); - - - -(function() { -Ember.Handlebars.registerHelper('form-for', function(object, options) { - options.hash.contentBinding = object; - return Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Form, options); -}); - -})(); - - - -(function() { -Ember.Handlebars.registerHelper('hint-field', function(property, options) { - options = Ember.EasyForm.processOptions(property, options); - - if (options.hash.text || options.hash.textBinding) { - return Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Hint, options); - } -}); - -})(); - - - -(function() { -Ember.Handlebars.registerHelper('input', function(property, options) { - options = Ember.EasyForm.processOptions(property, options); - options.hash.isBlock = !!(options.fn); - return Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Input, options); -}); - -})(); - - - -(function() { -Ember.Handlebars.registerHelper('input-field', function(property, options) { - options = Ember.EasyForm.processOptions(property, options); - - if (options.hash.propertyBinding) { - options.hash.property = Ember.Handlebars.get(this, options.hash.propertyBinding, options); - } - - if (options.hash.inputOptionsBinding) { - options.hash.inputOptions = Ember.Handlebars.get(this, options.hash.inputOptionsBinding, options); - } - - property = options.hash.property; - - var context = this, - propertyType = function(property) { - var constructor = (context.get('content') || context).constructor; - - if (constructor.proto) { - return Ember.meta(constructor.proto(), false).descs[property]; - } else { - return null; - } - }; - - options.hash.valueBinding = property; - options.hash.viewName = 'input-field-'+options.data.view.elementId; - - if (options.hash.inputOptions) { - var inputOptions = options.hash.inputOptions, optionName; - for (optionName in inputOptions) { - if (inputOptions.hasOwnProperty(optionName)) { - options.hash[optionName] = inputOptions[optionName]; - } - } - delete options.hash.inputOptions; - } - - if (options.hash.inputConfig) { - var configs = options.hash.inputConfig.split(';'); - var i = configs.length; - while(i--) { - var config = configs[i].split(':'); - options.hash[config[0]] = config[1]; - } - } - - if (options.hash.as === 'text') { - return Ember.Handlebars.helpers.view.call(context, Ember.EasyForm.TextArea, options); - } else if (options.hash.as === 'select') { - delete(options.hash.valueBinding); - - options.hash.contentBinding = options.hash.collection; - options.hash.selectionBinding = options.hash.selection; - options.hash.valueBinding = options.hash.value; - - if (Ember.isNone(options.hash.selectionBinding) && Ember.isNone(options.hash.valueBinding)) { - options.hash.selectionBinding = property; - } - - return Ember.Handlebars.helpers.view.call(context, Ember.EasyForm.Select, options); - } else if (options.hash.as === 'checkbox') { - if (Ember.isNone(options.hash.checkedBinding)) { - options.hash.checkedBinding = property; - } - - return Ember.Handlebars.helpers.view.call(context, Ember.EasyForm.Checkbox, options); - } else { - if (!options.hash.as) { - if (property.match(/password/)) { - options.hash.type = 'password'; - } else if (property.match(/email/)) { - options.hash.type = 'email'; - } else if (property.match(/url/)) { - options.hash.type = 'url'; - } else if (property.match(/color/)) { - options.hash.type = 'color'; - } else if (property.match(/^tel/)) { - options.hash.type = 'tel'; - } else if (property.match(/search/)) { - options.hash.type = 'search'; - } else { - if (propertyType(property) === 'number' || typeof(context.get(property)) === 'number') { - options.hash.type = 'number'; - } else if (propertyType(property) === 'date' || (!Ember.isNone(context.get(property)) && context.get(property).constructor === Date)) { - options.hash.type = 'date'; - } else if (propertyType(property) === 'boolean' || (!Ember.isNone(context.get(property)) && context.get(property).constructor === Boolean)) { - options.hash.checkedBinding = property; - return Ember.Handlebars.helpers.view.call(context, Ember.EasyForm.Checkbox, options); - } - } - } else { - var inputType = Ember.EasyForm.Config.getInputType(options.hash.as); - if (inputType) { - return Ember.Handlebars.helpers.view.call(context, inputType, options); - } - - options.hash.type = options.hash.as; - } - return Ember.Handlebars.helpers.view.call(context, Ember.EasyForm.TextField, options); - } -}); - -})(); - - - -(function() { -Ember.Handlebars.registerHelper('label-field', function(property, options) { - options = Ember.EasyForm.processOptions(property, options); - options.hash.viewName = 'label-field-'+options.data.view.elementId; - return Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Label, options); -}); - -})(); - - - -(function() { -Ember.Handlebars.registerHelper('submit', function(value, options) { - if (typeof(value) === 'object') { - options = value; - value = undefined; - } - options.hash.context = this; - options.hash.value = value || 'Submit'; - return (options.hash.as === 'button') ? - Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Button, options) - : - Ember.Handlebars.helpers.view.call(this, Ember.EasyForm.Submit, options); -}); - -})(); - - - -(function() { - -})(); - - - -(function() { -Ember.EasyForm.BaseView = Ember.View.extend({ - getWrapperConfig: function(configName) { - var wrapper = Ember.EasyForm.Config.getWrapper(this.get('wrapper')); - return wrapper[configName]; - }, - wrapper: Ember.computed(function() { - // Find the first parent with 'wrapper' defined. - var parentView = this.get('parentView'); - while(parentView){ - var config = parentView.get('wrapper'); - if (config) return config; - parentView = parentView.get('parentView'); - } - - return 'default'; - }) -}); -})(); - - - -(function() { -Ember.EasyForm.Checkbox = Ember.Checkbox.extend(); - -})(); - - - -(function() { -Ember.EasyForm.Error = Ember.EasyForm.BaseView.extend({ - tagName: 'span', - init: function() { - this._super(); - this.classNames.push(this.getWrapperConfig('errorClass')); - Ember.Binding.from('context.errors.' + this.property).to('errors').connect(this); - }, - templateName: 'easyForm/error' -}); - -})(); - - - -(function() { -Ember.EasyForm.Form = Ember.EasyForm.BaseView.extend({ - tagName: 'form', - attributeBindings: ['novalidate'], - novalidate: 'novalidate', - wrapper: 'default', - init: function() { - this._super(); - this.classNames.push(this.getWrapperConfig('formClass')); - this.action = this.action || 'submit'; - }, - submit: function(event) { - var _this = this, promise; - - if (event) { - event.preventDefault(); - } - - if (Ember.isNone(this.get('context.validate'))) { - this.get('controller').send(this.action); - } else { - if (!Ember.isNone(this.get('context').validate)) { - promise = this.get('context').validate(); - } else { - promise = this.get('context.content').validate(); - } - promise.then(function() { - if (_this.get('context.isValid')) { - _this.get('controller').send(_this.action); - } - }); - } - } -}); - -})(); - - - -(function() { -Ember.EasyForm.Hint = Ember.EasyForm.BaseView.extend({ - tagName: 'span', - init: function() { - this._super(); - this.classNames.push(this.getWrapperConfig('hintClass')); - }, - render: function(buffer) { - buffer.push(Handlebars.Utils.escapeExpression(this.get('text'))); - }, - textChanged: function() { - this.rerender(); - }.observes('text') -}); - -})(); - - - -(function() { -Ember.EasyForm.Input = Ember.EasyForm.BaseView.extend({ - init: function() { - this._super(); - this.classNameBindings.push('showError:' + this.getWrapperConfig('fieldErrorClass')); - this.classNames.push(this.getWrapperConfig('inputClass')); - Ember.defineProperty(this, 'showError', Ember.computed.and('canShowValidationError', 'context.errors.' + this.property + '.firstObject')); - if (!this.isBlock) { - if (this.getWrapperConfig('wrapControls')) { - this.set('templateName', 'easyForm/wrapped_input'); - } else { - this.set('templateName', 'easyForm/input'); - } - } - }, - setupValidationDependencies: function() { - var keys = this.get('context._dependentValidationKeys'), key; - if (keys) { - for(key in keys) { - if (keys[key].contains(this.property)) { - this._keysForValidationDependencies.pushObject(key); - } - } - } - }.on('init'), - _keysForValidationDependencies: Ember.A(), - dependentValidationKeyCanTrigger: false, - tagName: 'div', - classNames: ['string'], - didInsertElement: function() { - this.set('label-field-'+this.elementId+'.for', this.get('input-field-'+this.elementId+'.elementId')); - }, - concatenatedProperties: ['inputOptions', 'bindableInputOptions'], - inputOptions: ['as', 'inputConfig', 'collection', 'optionValuePath', 'optionLabelPath', 'selection', 'value', 'multiple'], - bindableInputOptions: ['placeholder', 'prompt'], - controlsWrapperClass: function() { - return this.getWrapperConfig('controlsWrapperClass'); - }.property(), - inputOptionsValues: function() { - var options = {}, i, key, keyBinding, inputOptions = this.inputOptions, bindableInputOptions = this.bindableInputOptions; - for (i = 0; i < inputOptions.length; i++) { - key = inputOptions[i]; - if (this[key]) { - if (typeof(this[key]) === 'boolean') { - this[key] = key; - } - - options[key] = this[key]; - } - } - for (i = 0; i < bindableInputOptions.length; i++) { - key = bindableInputOptions[i]; - keyBinding = key + 'Binding'; - if (this[key] || this[keyBinding]) { - options[keyBinding] = 'view.' + key; - } - } - return options; - }.property(), - focusOut: function() { - this.set('hasFocusedOut', true); - this.showValidationError(); - }, - showValidationError: function() { - if (this.get('hasFocusedOut')) { - if (Ember.isEmpty(this.get('context.errors.' + this.property))) { - this.set('canShowValidationError', false); - } else { - this.set('canShowValidationError', true); - } - } - }, - input: function() { - this._keysForValidationDependencies.forEach(function(key) { - this.get('parentView.childViews').forEach(function(view) { - if (view.property === key) { - view.showValidationError(); - } - }, this); - }, this); - } -}); - -})(); - - - -(function() { -Ember.EasyForm.Label = Ember.EasyForm.BaseView.extend({ - tagName: 'label', - attributeBindings: ['for'], - labelText: function() { - return this.get('text') || this.get('property').underscore().split('_').join(' ').capitalize(); - }.property('text', 'property'), - init: function() { - this._super(); - this.classNames.push(this.getWrapperConfig('labelClass')); - }, - render: function(buffer) { - buffer.push(Handlebars.Utils.escapeExpression(this.get('labelText'))); - }, - labelTextChanged: function() { - this.rerender(); - }.observes('labelText') -}); - -})(); - - - -(function() { -Ember.EasyForm.Select = Ember.Select.extend(); - -})(); - - - -(function() { -Ember.EasyForm.Submit = Ember.View.extend({ - tagName: 'input', - attributeBindings: ['type', 'value', 'disabled'], - type: 'submit', - disabled: function() { - return this.get('context.isInvalid'); - }.property('context.isInvalid'), - init: function() { - this._super(); - this.set('value', this.value); - } -}); - -})(); - - - -(function() { -Ember.EasyForm.Button = Ember.View.extend({ - tagName: 'button', - template: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [4,'>= 1.0.0']; -helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; - var hashTypes, hashContexts, escapeExpression=this.escapeExpression; - - - hashTypes = {}; - hashContexts = {}; - data.buffer.push(escapeExpression(helpers._triageMustache.call(depth0, "text", {hash:{},contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); - -}), - attributeBindings: ['type', 'disabled'], - type: 'submit', - disabled: function() { - return this.get('context.isInvalid'); - }.property('context.isInvalid'), - init: function() { - this._super(); - this.set('context.text', this.value); - } -}); - -})(); - - - -(function() { -Ember.EasyForm.TextArea = Ember.TextArea.extend(); - -})(); - - - -(function() { -Ember.EasyForm.TextField = Ember.TextField.extend(); - -})(); - - - -(function() { - -})(); - - - -(function() { -Ember.TEMPLATES['easyForm/error'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [4,'>= 1.0.0']; -helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; - var hashTypes, hashContexts, escapeExpression=this.escapeExpression; - - - hashTypes = {}; - hashContexts = {}; - data.buffer.push(escapeExpression(helpers._triageMustache.call(depth0, "view.errors.firstObject", {hash:{},contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); - -}); - -})(); - - - -(function() { -Ember.TEMPLATES['easyForm/input'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [4,'>= 1.0.0']; -helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; - var buffer = '', stack1, hashContexts, hashTypes, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression; - - - hashContexts = {'propertyBinding': depth0,'textBinding': depth0}; - hashTypes = {'propertyBinding': "STRING",'textBinding': "STRING"}; - options = {hash:{ - 'propertyBinding': ("view.property"), - 'textBinding': ("view.label") - },contexts:[],types:[],hashContexts:hashContexts,hashTypes:hashTypes,data:data}; - data.buffer.push(escapeExpression(((stack1 = helpers['label-field'] || depth0['label-field']),stack1 ? stack1.call(depth0, options) : helperMissing.call(depth0, "label-field", options)))); - hashTypes = {}; - hashContexts = {}; - options = {hash:{},contexts:[depth0],types:["STRING"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}; - data.buffer.push(escapeExpression(((stack1 = helpers.partial || depth0.partial),stack1 ? stack1.call(depth0, "easyForm/inputControls", options) : helperMissing.call(depth0, "partial", "easyForm/inputControls", options)))); - return buffer; - -}); - -})(); - - - -(function() { -Ember.TEMPLATES['easyForm/inputControls'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [4,'>= 1.0.0']; -helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; - var buffer = '', stack1, stack2, hashContexts, hashTypes, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, self=this; - -function program1(depth0,data) { - - var stack1, hashContexts, hashTypes, options; - hashContexts = {'propertyBinding': depth0}; - hashTypes = {'propertyBinding': "STRING"}; - options = {hash:{ - 'propertyBinding': ("view.property") - },contexts:[],types:[],hashContexts:hashContexts,hashTypes:hashTypes,data:data}; - data.buffer.push(escapeExpression(((stack1 = helpers['error-field'] || depth0['error-field']),stack1 ? stack1.call(depth0, options) : helperMissing.call(depth0, "error-field", options)))); - } - -function program3(depth0,data) { - - var stack1, hashContexts, hashTypes, options; - hashContexts = {'propertyBinding': depth0,'textBinding': depth0}; - hashTypes = {'propertyBinding': "STRING",'textBinding': "STRING"}; - options = {hash:{ - 'propertyBinding': ("view.property"), - 'textBinding': ("view.hint") - },contexts:[],types:[],hashContexts:hashContexts,hashTypes:hashTypes,data:data}; - data.buffer.push(escapeExpression(((stack1 = helpers['hint-field'] || depth0['hint-field']),stack1 ? stack1.call(depth0, options) : helperMissing.call(depth0, "hint-field", options)))); - } - - hashContexts = {'propertyBinding': depth0,'inputOptionsBinding': depth0}; - hashTypes = {'propertyBinding': "STRING",'inputOptionsBinding': "STRING"}; - options = {hash:{ - 'propertyBinding': ("view.property"), - 'inputOptionsBinding': ("view.inputOptionsValues") - },contexts:[],types:[],hashContexts:hashContexts,hashTypes:hashTypes,data:data}; - data.buffer.push(escapeExpression(((stack1 = helpers['input-field'] || depth0['input-field']),stack1 ? stack1.call(depth0, options) : helperMissing.call(depth0, "input-field", options)))); - hashTypes = {}; - hashContexts = {}; - stack2 = helpers['if'].call(depth0, "view.showError", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); - if(stack2 || stack2 === 0) { data.buffer.push(stack2); } - hashTypes = {}; - hashContexts = {}; - stack2 = helpers['if'].call(depth0, "view.hint", {hash:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); - if(stack2 || stack2 === 0) { data.buffer.push(stack2); } - return buffer; - -}); - -})(); - - - -(function() { -Ember.TEMPLATES['easyForm/wrapped_input'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [4,'>= 1.0.0']; -helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; - var buffer = '', stack1, hashContexts, hashTypes, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression; - - - hashContexts = {'propertyBinding': depth0,'textBinding': depth0}; - hashTypes = {'propertyBinding': "STRING",'textBinding': "STRING"}; - options = {hash:{ - 'propertyBinding': ("view.property"), - 'textBinding': ("view.label") - },contexts:[],types:[],hashContexts:hashContexts,hashTypes:hashTypes,data:data}; - data.buffer.push(escapeExpression(((stack1 = helpers['label-field'] || depth0['label-field']),stack1 ? stack1.call(depth0, options) : helperMissing.call(depth0, "label-field", options)))); - data.buffer.push("

"); - hashTypes = {}; - hashContexts = {}; - options = {hash:{},contexts:[depth0],types:["STRING"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}; - data.buffer.push(escapeExpression(((stack1 = helpers.partial || depth0.partial),stack1 ? stack1.call(depth0, "easyForm/inputControls", options) : helperMissing.call(depth0, "partial", "easyForm/inputControls", options)))); - data.buffer.push("
"); - return buffer; - -}); - -})(); - - - -(function() { -Ember.EasyForm.TEMPLATES = {}; - -})(); - - - -(function() { -Ember.EasyForm.objectNameFor = function(object) { - var constructorArray = object.constructor.toString().split('.'); - return constructorArray[constructorArray.length - 1].underscore(); -}; - -Ember.EasyForm.processOptions = function(property, options) { - if (options) { - options.hash.property = property; - } else { - options = property; - } - - return options; -}; - -})(); - - - -(function() { - -})(); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-model/ember-model.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-model/ember-model.js deleted file mode 100644 index b0c092cca..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-model/ember-model.js +++ /dev/null @@ -1,2037 +0,0 @@ -(function() { - -var VERSION = '0.0.11'; - -if (Ember.libraries) { - Ember.libraries.register('Ember Model', VERSION); -} - - -})(); - -(function() { - -function mustImplement(message) { - var fn = function() { - var className = this.constructor.toString(); - - throw new Error(message.replace('{{className}}', className)); - }; - fn.isUnimplemented = true; - return fn; -} - -Ember.Adapter = Ember.Object.extend({ - find: mustImplement('{{className}} must implement find'), - findQuery: mustImplement('{{className}} must implement findQuery'), - findMany: mustImplement('{{className}} must implement findMany'), - findAll: mustImplement('{{className}} must implement findAll'), - createRecord: mustImplement('{{className}} must implement createRecord'), - saveRecord: mustImplement('{{className}} must implement saveRecord'), - deleteRecord: mustImplement('{{className}} must implement deleteRecord'), - - load: function(record, id, data) { - record.load(id, data); - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set; - -Ember.FixtureAdapter = Ember.Adapter.extend({ - _counter: 0, - _findData: function(klass, id) { - var fixtures = klass.FIXTURES, - idAsString = id.toString(), - primaryKey = get(klass, 'primaryKey'), - data = Ember.A(fixtures).find(function(el) { return (el[primaryKey]).toString() === idAsString; }); - - return data; - }, - - _setPrimaryKey: function(record) { - var klass = record.constructor, - fixtures = klass.FIXTURES, - primaryKey = get(klass, 'primaryKey'); - - - if(record.get(primaryKey)) { - return; - } - - set(record, primaryKey, this._generatePrimaryKey()); - }, - - _generatePrimaryKey: function() { - var counter = this.get("_counter"); - - this.set("_counter", counter + 1); - - return "fixture-" + counter; - }, - - find: function(record, id) { - var data = this._findData(record.constructor, id); - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - Ember.run(record, record.load, id, data); - resolve(record); - }, 0); - }); - }, - - findMany: function(klass, records, ids) { - var fixtures = klass.FIXTURES, - requestedData = []; - - for (var i = 0, l = ids.length; i < l; i++) { - requestedData.push(this._findData(klass, ids[i])); - } - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - Ember.run(records, records.load, klass, requestedData); - resolve(records); - }, 0); - }); - }, - - findAll: function(klass, records) { - var fixtures = klass.FIXTURES; - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - Ember.run(records, records.load, klass, fixtures); - resolve(records); - }, 0); - }); - }, - - createRecord: function(record) { - var klass = record.constructor, - fixtures = klass.FIXTURES, - self = this; - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - self._setPrimaryKey(record); - fixtures.push(klass.findFromCacheOrLoad(record.toJSON())); - record.didCreateRecord(); - resolve(record); - }, 0); - }); - }, - - saveRecord: function(record) { - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - record.didSaveRecord(); - resolve(record); - }, 0); - }); - }, - - deleteRecord: function(record) { - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - record.didDeleteRecord(); - resolve(record); - }, 0); - }); - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set; - -Ember.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, { - isLoaded: false, - isLoading: Ember.computed.not('isLoaded'), - - load: function(klass, data) { - set(this, 'content', this.materializeData(klass, data)); - this.notifyLoaded(); - }, - - loadForFindMany: function(klass) { - var self = this; - var content = get(this, '_ids').map(function(id) { return klass.cachedRecordForId(id, self.container); }); - set(this, 'content', Ember.A(content)); - this.notifyLoaded(); - }, - - notifyLoaded: function() { - set(this, 'isLoaded', true); - this.trigger('didLoad'); - }, - - materializeData: function(klass, data) { - var self = this; - return Ember.A(data.map(function(el) { - return klass.findFromCacheOrLoad(el, self.container); // FIXME - })); - }, - - reload: function() { - var modelClass = this.get('modelClass'), - self = this, - promises; - - set(this, 'isLoaded', false); - if (modelClass._findAllRecordArray === this) { - return modelClass.adapter.findAll(modelClass, this); - } else if (this._query) { - return modelClass.adapter.findQuery(modelClass, this, this._query); - } else { - promises = this.map(function(record) { - return record.reload(); - }); - return Ember.RSVP.all(promises).then(function(data) { - self.notifyLoaded(); - }); - } - } -}); - - -})(); - -(function() { - -var get = Ember.get; - -Ember.FilteredRecordArray = Ember.RecordArray.extend({ - init: function() { - if (!get(this, 'modelClass')) { - throw new Error('FilteredRecordArrays must be created with a modelClass'); - } - if (!get(this, 'filterFunction')) { - throw new Error('FilteredRecordArrays must be created with a filterFunction'); - } - if (!get(this, 'filterProperties')) { - throw new Error('FilteredRecordArrays must be created with filterProperties'); - } - - var modelClass = get(this, 'modelClass'); - modelClass.registerRecordArray(this); - - this.registerObservers(); - this.updateFilter(); - - this._super(); - }, - - updateFilter: function() { - var self = this, - results = []; - get(this, 'modelClass').forEachCachedRecord(function(record) { - if (self.filterFunction(record)) { - results.push(record); - } - }); - this.set('content', Ember.A(results)); - }, - - updateFilterForRecord: function(record) { - var results = get(this, 'content'); - if (this.filterFunction(record) && !results.contains(record)) { - results.pushObject(record); - } - }, - - registerObservers: function() { - var self = this; - get(this, 'modelClass').forEachCachedRecord(function(record) { - self.registerObserversOnRecord(record); - }); - }, - - registerObserversOnRecord: function(record) { - var self = this, - filterProperties = get(this, 'filterProperties'); - - for (var i = 0, l = get(filterProperties, 'length'); i < l; i++) { - record.addObserver(filterProperties[i], self, 'updateFilterForRecord'); - } - } -}); - -})(); - -(function() { - -var get = Ember.get, set = Ember.set; - -Ember.ManyArray = Ember.RecordArray.extend({ - _records: null, - originalContent: null, - _modifiedRecords: null, - - unloadObject: function(record) { - var obj = get(this, 'content').findBy('clientId', record._reference.clientId); - get(this, 'content').removeObject(obj); - - var originalObj = get(this, 'originalContent').findBy('clientId', record._reference.clientId); - get(this, 'originalContent').removeObject(originalObj); - }, - - isDirty: function() { - var originalContent = get(this, 'originalContent'), - originalContentLength = get(originalContent, 'length'), - content = get(this, 'content'), - contentLength = get(content, 'length'); - - if (originalContentLength !== contentLength) { return true; } - - if (this._modifiedRecords && this._modifiedRecords.length) { return true; } - - var isDirty = false; - - for (var i = 0, l = contentLength; i < l; i++) { - if (!originalContent.contains(content[i])) { - isDirty = true; - break; - } - } - - return isDirty; - }.property('content.[]', 'originalContent.[]', '_modifiedRecords.[]'), - - objectAtContent: function(idx) { - var content = get(this, 'content'); - - if (!content.length) { return; } - - // need to add observer if it wasn't materialized before - var observerNeeded = (content[idx].record) ? false : true; - - var record = this.materializeRecord(idx, this.container); - - if (observerNeeded) { - var isDirtyRecord = record.get('isDirty'), isNewRecord = record.get('isNew'); - if (isDirtyRecord || isNewRecord) { this._modifiedRecords.pushObject(content[idx]); } - Ember.addObserver(content[idx], 'record.isDirty', this, 'recordStateChanged'); - record.registerParentHasManyArray(this); - } - - return record; - }, - - save: function() { - // TODO: loop over dirty records only - return Ember.RSVP.all(this.map(function(record) { - return record.save(); - })); - }, - - replaceContent: function(index, removed, added) { - added = Ember.EnumerableUtils.map(added, function(record) { - return record._reference; - }, this); - - this._super(index, removed, added); - }, - - _contentWillChange: function() { - var content = get(this, 'content'); - - if (content) { - this.arrayWillChange(content, 0, get(content, 'length'), 0); - content.removeArrayObserver(this); - this._setupOriginalContent(content); - } - }.observesBefore('content'), - - _contentDidChange: function() { - var content = get(this, 'content'); - if (content) { - content.addArrayObserver(this); - this.arrayDidChange(content, 0, 0, get(content, 'length')); - } - }.observes('content'), - - arrayWillChange: function(item, idx, removedCnt, addedCnt) { - var content = item; - for (var i = idx; i < idx+removedCnt; i++) { - var currentItem = content[i]; - if (currentItem && currentItem.record) { - this._modifiedRecords.removeObject(currentItem); - currentItem.record.unregisterParentHasManyArray(this); - Ember.removeObserver(currentItem, 'record.isDirty', this, 'recordStateChanged'); - } - } - }, - - arrayDidChange: function(item, idx, removedCnt, addedCnt) { - var parent = get(this, 'parent'), relationshipKey = get(this, 'relationshipKey'), - isDirty = get(this, 'isDirty'); - - var content = item; - for (var i = idx; i < idx+addedCnt; i++) { - var currentItem = content[i]; - if (currentItem && currentItem.record) { - var isDirtyRecord = currentItem.record.get('isDirty'), isNewRecord = currentItem.record.get('isNew'); // why newly created object is not dirty? - if (isDirtyRecord || isNewRecord) { this._modifiedRecords.pushObject(currentItem); } - Ember.addObserver(currentItem, 'record.isDirty', this, 'recordStateChanged'); - currentItem.record.registerParentHasManyArray(this); - } - } - - if (isDirty) { - parent._relationshipBecameDirty(relationshipKey); - } else { - parent._relationshipBecameClean(relationshipKey); - } - }, - - load: function(content) { - Ember.setProperties(this, { - content: content, - originalContent: content.slice() - }); - set(this, '_modifiedRecords', []); - }, - - revert: function() { - this._setupOriginalContent(); - }, - - _setupOriginalContent: function(content) { - content = content || get(this, 'content'); - if (content) { - set(this, 'originalContent', content.slice()); - } - set(this, '_modifiedRecords', []); - }, - - init: function() { - this._super(); - this._setupOriginalContent(); - this._contentDidChange(); - }, - - recordStateChanged: function(obj, keyName) { - var parent = get(this, 'parent'), relationshipKey = get(this, 'relationshipKey'); - - if (obj.record.get('isDirty')) { - if (this._modifiedRecords.indexOf(obj) === -1) { this._modifiedRecords.pushObject(obj); } - parent._relationshipBecameDirty(relationshipKey); - } else { - if (this._modifiedRecords.indexOf(obj) > -1) { this._modifiedRecords.removeObject(obj); } - if (!this.get('isDirty')) { - parent._relationshipBecameClean(relationshipKey); - } - } - } -}); - -Ember.HasManyArray = Ember.ManyArray.extend({ - materializeRecord: function(idx, container) { - var klass = get(this, 'modelClass'), - content = get(this, 'content'), - reference = content.objectAt(idx), - record; - - if (reference.record) { - record = reference.record; - } else { - record = klass.find(reference.id); - } - - record.container = container; - return record; - }, - - toJSON: function() { - var ids = [], content = this.get('content'); - - content.forEach(function(reference) { - if (reference.id) { - ids.push(reference.id); - } - }); - - return ids; - } -}); - -Ember.EmbeddedHasManyArray = Ember.ManyArray.extend({ - create: function(attrs) { - var klass = get(this, 'modelClass'), - record = klass.create(attrs); - - this.pushObject(record); - - return record; // FIXME: inject parent's id - }, - - materializeRecord: function(idx, container) { - var klass = get(this, 'modelClass'), - primaryKey = get(klass, 'primaryKey'), - content = get(this, 'content'), - reference = content.objectAt(idx), - attrs = reference.data; - - var record; - if (reference.record) { - record = reference.record; - } else { - record = klass.create({ _reference: reference, container: container }); - reference.record = record; - if (attrs) { - record.load(attrs[primaryKey], attrs); - } - } - - record.container = container; - return record; - }, - - toJSON: function() { - return this.map(function(record) { - return record.toJSON(); - }); - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set, - setProperties = Ember.setProperties, - meta = Ember.meta, - underscore = Ember.String.underscore; - -function contains(array, element) { - for (var i = 0, l = array.length; i < l; i++) { - if (array[i] === element) { return true; } - } - return false; -} - -function concatUnique(toArray, fromArray) { - var e; - for (var i = 0, l = fromArray.length; i < l; i++) { - e = fromArray[i]; - if (!contains(toArray, e)) { toArray.push(e); } - } - return toArray; -} - -function hasCachedValue(object, key) { - var objectMeta = meta(object, false); - if (objectMeta) { - return key in objectMeta.cache; - } -} - -Ember.run.queues.push('data'); - -Ember.Model = Ember.Object.extend(Ember.Evented, { - isLoaded: true, - isLoading: Ember.computed.not('isLoaded'), - isNew: true, - isDeleted: false, - _dirtyAttributes: null, - - /** - Called when attribute is accessed. - - @method getAttr - @param key {String} key which is being accessed - @param value {Object} value, which will be returned from getter by default - */ - getAttr: function(key, value) { - return value; - }, - - isDirty: function() { - var dirtyAttributes = get(this, '_dirtyAttributes'); - return dirtyAttributes && dirtyAttributes.length !== 0 || false; - }.property('_dirtyAttributes.length'), - - _relationshipBecameDirty: function(name) { - var dirtyAttributes = get(this, '_dirtyAttributes'); - if (!dirtyAttributes.contains(name)) { dirtyAttributes.pushObject(name); } - }, - - _relationshipBecameClean: function(name) { - var dirtyAttributes = get(this, '_dirtyAttributes'); - dirtyAttributes.removeObject(name); - }, - - dataKey: function(key) { - var camelizeKeys = get(this.constructor, 'camelizeKeys'); - var meta = this.constructor.metaForProperty(key); - if (meta.options && meta.options.key) { - return camelizeKeys ? underscore(meta.options.key) : meta.options.key; - } - return camelizeKeys ? underscore(key) : key; - }, - - init: function() { - this._createReference(); - if (!this._dirtyAttributes) { - set(this, '_dirtyAttributes', []); - } - this._super(); - }, - - _createReference: function() { - var reference = this._reference, - id = this.getPrimaryKey(); - - if (!reference) { - reference = this.constructor._getOrCreateReferenceForId(id); - reference.record = this; - this._reference = reference; - } else if (reference.id !== id) { - reference.id = id; - this.constructor._cacheReference(reference); - } - - if (!reference.id) { - reference.id = id; - } - - return reference; - }, - - getPrimaryKey: function() { - return get(this, get(this.constructor, 'primaryKey')); - }, - - load: function(id, hash) { - var data = {}; - data[get(this.constructor, 'primaryKey')] = id; - set(this, '_data', Ember.merge(data, hash)); - this.getWithDefault('_dirtyAttributes', []).clear(); - - this._reloadHasManys(); - - // eagerly load embedded data - var relationships = this.constructor._relationships || [], meta = Ember.meta(this), relationshipKey, relationship, relationshipMeta, relationshipData, relationshipType; - for (var i = 0, l = relationships.length; i < l; i++) { - relationshipKey = relationships[i]; - relationship = meta.descs[relationshipKey]; - relationshipMeta = relationship.meta(); - - if (relationshipMeta.options.embedded) { - relationshipType = relationshipMeta.type; - if (typeof relationshipType === "string") { - relationshipType = Ember.get(Ember.lookup, relationshipType) || this.container.lookupFactory('model:'+ relationshipType); - } - - relationshipData = data[relationshipKey]; - if (relationshipData) { - relationshipType.load(relationshipData); - } - } - } - - set(this, 'isNew', false); - set(this, 'isLoaded', true); - this._createReference(); - this.trigger('didLoad'); - }, - - didDefineProperty: function(proto, key, value) { - if (value instanceof Ember.Descriptor) { - var meta = value.meta(); - var klass = proto.constructor; - - if (meta.isAttribute) { - if (!klass._attributes) { klass._attributes = []; } - klass._attributes.push(key); - } else if (meta.isRelationship) { - if (!klass._relationships) { klass._relationships = []; } - klass._relationships.push(key); - meta.relationshipKey = key; - } - } - }, - - serializeHasMany: function(key, meta) { - return this.get(key).toJSON(); - }, - - serializeBelongsTo: function(key, meta) { - if (meta.options.embedded) { - var record = this.get(key); - return record ? record.toJSON() : null; - } else { - var primaryKey = get(meta.getType(), 'primaryKey'); - return this.get(key + '.' + primaryKey); - } - }, - - toJSON: function() { - var key, meta, - json = {}, - attributes = this.constructor.getAttributes(), - relationships = this.constructor.getRelationships(), - properties = attributes ? this.getProperties(attributes) : {}, - rootKey = get(this.constructor, 'rootKey'); - - for (key in properties) { - meta = this.constructor.metaForProperty(key); - if (meta.type && meta.type.serialize) { - json[this.dataKey(key)] = meta.type.serialize(properties[key]); - } else if (meta.type && Ember.Model.dataTypes[meta.type]) { - json[this.dataKey(key)] = Ember.Model.dataTypes[meta.type].serialize(properties[key]); - } else { - json[this.dataKey(key)] = properties[key]; - } - } - - if (relationships) { - var data, relationshipKey; - - for(var i = 0; i < relationships.length; i++) { - key = relationships[i]; - meta = this.constructor.metaForProperty(key); - relationshipKey = meta.options.key || key; - - if (meta.kind === 'belongsTo') { - data = this.serializeBelongsTo(key, meta); - } else { - data = this.serializeHasMany(key, meta); - } - - json[relationshipKey] = data; - - } - } - - if (rootKey) { - var jsonRoot = {}; - jsonRoot[rootKey] = json; - return jsonRoot; - } else { - return json; - } - }, - - save: function() { - var adapter = this.constructor.adapter; - set(this, 'isSaving', true); - if (get(this, 'isNew')) { - return adapter.createRecord(this); - } else if (get(this, 'isDirty')) { - return adapter.saveRecord(this); - } else { // noop, return a resolved promise - var self = this, - promise = new Ember.RSVP.Promise(function(resolve, reject) { - resolve(self); - }); - set(this, 'isSaving', false); - return promise; - } - }, - - reload: function() { - this.getWithDefault('_dirtyAttributes', []).clear(); - return this.constructor.reload(this.get(get(this.constructor, 'primaryKey')), this.container); - }, - - revert: function() { - this.getWithDefault('_dirtyAttributes', []).clear(); - this.notifyPropertyChange('_data'); - this._reloadHasManys(true); - }, - - didCreateRecord: function() { - var primaryKey = get(this.constructor, 'primaryKey'), - id = get(this, primaryKey); - - set(this, 'isNew', false); - - set(this, '_dirtyAttributes', []); - this.constructor.addToRecordArrays(this); - this.trigger('didCreateRecord'); - this.didSaveRecord(); - }, - - didSaveRecord: function() { - set(this, 'isSaving', false); - this.trigger('didSaveRecord'); - if (this.get('isDirty')) { this._copyDirtyAttributesToData(); } - }, - - deleteRecord: function() { - return this.constructor.adapter.deleteRecord(this); - }, - - didDeleteRecord: function() { - this.constructor.removeFromRecordArrays(this); - set(this, 'isDeleted', true); - this.trigger('didDeleteRecord'); - }, - - _copyDirtyAttributesToData: function() { - if (!this._dirtyAttributes) { return; } - var dirtyAttributes = this._dirtyAttributes, - data = get(this, '_data'), - key; - - if (!data) { - data = {}; - set(this, '_data', data); - } - for (var i = 0, l = dirtyAttributes.length; i < l; i++) { - // TODO: merge Object.create'd object into prototype - key = dirtyAttributes[i]; - data[this.dataKey(key)] = this.cacheFor(key); - } - set(this, '_dirtyAttributes', []); - this._resetDirtyStateInNestedObjects(this); // we need to reset isDirty state to all child objects in embedded relationships - }, - - _resetDirtyStateInNestedObjects: function(object) { - var i, obj; - if (object._hasManyArrays) { - for (i = 0; i < object._hasManyArrays.length; i++) { - var array = object._hasManyArrays[i]; - array.revert(); - if (array.embedded) { - for (var j = 0; j < array.get('length'); j++) { - obj = array.objectAt(j); - obj._copyDirtyAttributesToData(); - } - } - } - } - - if (object._belongsTo) { - for (i = 0; i < object._belongsTo.length; i++) { - var belongsTo = object._belongsTo[i]; - if (belongsTo.options.embedded) { - obj = this.get(belongsTo.relationshipKey); - if (obj) { - obj._copyDirtyAttributesToData(); - } - } - } - } - }, - - _registerHasManyArray: function(array) { - if (!this._hasManyArrays) { this._hasManyArrays = Ember.A([]); } - - this._hasManyArrays.pushObject(array); - }, - - registerParentHasManyArray: function(array) { - if (!this._parentHasManyArrays) { this._parentHasManyArrays = Ember.A([]); } - - this._parentHasManyArrays.pushObject(array); - }, - - unregisterParentHasManyArray: function(array) { - if (!this._parentHasManyArrays) { return; } - - this._parentHasManyArrays.removeObject(array); - }, - - _reloadHasManys: function(reverting) { - if (!this._hasManyArrays) { return; } - var i, j; - for (i = 0; i < this._hasManyArrays.length; i++) { - var array = this._hasManyArrays[i], - hasManyContent = this._getHasManyContent(get(array, 'key'), get(array, 'modelClass'), get(array, 'embedded')); - if (!reverting) { - for (j = 0; j < array.get('length'); j++) { - if (array.objectAt(j).get('isNew') && !array.objectAt(j).get('isDeleted')) { - hasManyContent.addObject(array.objectAt(j)._reference); - } - } - } - array.load(hasManyContent); - } - }, - - _getHasManyContent: function(key, type, embedded) { - var content = get(this, '_data.' + key); - - if (content) { - var mapFunction, primaryKey, reference; - if (embedded) { - primaryKey = get(type, 'primaryKey'); - mapFunction = function(attrs) { - reference = type._getOrCreateReferenceForId(attrs[primaryKey]); - reference.data = attrs; - return reference; - }; - } else { - mapFunction = function(id) { return type._getOrCreateReferenceForId(id); }; - } - content = Ember.EnumerableUtils.map(content, mapFunction); - } - - return Ember.A(content || []); - }, - - _registerBelongsTo: function(key) { - if (!this._belongsTo) { this._belongsTo = Ember.A([]); } - - this._belongsTo.pushObject(key); - } -}); - -Ember.Model.reopenClass({ - primaryKey: 'id', - - adapter: Ember.Adapter.create(), - - _clientIdCounter: 1, - - getAttributes: function() { - this.proto(); // force class "compilation" if it hasn't been done. - var attributes = this._attributes || []; - if (typeof this.superclass.getAttributes === 'function') { - attributes = this.superclass.getAttributes().concat(attributes); - } - return attributes; - }, - - getRelationships: function() { - this.proto(); // force class "compilation" if it hasn't been done. - var relationships = this._relationships || []; - if (typeof this.superclass.getRelationships === 'function') { - relationships = this.superclass.getRelationships().concat(relationships); - } - return relationships; - }, - - fetch: function(id) { - if (!arguments.length) { - return this._findFetchAll(true); - } else if (Ember.isArray(id)) { - return this._findFetchMany(id, true); - } else if (typeof id === 'object') { - return this._findFetchQuery(id, true); - } else { - return this._findFetchById(id, true); - } - }, - - find: function(id) { - if (!arguments.length) { - return this._findFetchAll(false); - } else if (Ember.isArray(id)) { - return this._findFetchMany(id, false); - } else if (typeof id === 'object') { - return this._findFetchQuery(id, false); - } else { - return this._findFetchById(id, false); - } - }, - - findQuery: function(params) { - return this._findFetchQuery(params, false); - }, - - fetchQuery: function(params) { - return this._findFetchQuery(params, true); - }, - - _findFetchQuery: function(params, isFetch, container) { - var records = Ember.RecordArray.create({modelClass: this, _query: params, container: container}); - - var promise = this.adapter.findQuery(this, records, params); - - return isFetch ? promise : records; - }, - - findMany: function(ids) { - return this._findFetchMany(ids, false); - }, - - fetchMany: function(ids) { - return this._findFetchMany(ids, true); - }, - - _findFetchMany: function(ids, isFetch, container) { - Ember.assert("findFetchMany requires an array", Ember.isArray(ids)); - - var records = Ember.RecordArray.create({_ids: ids, modelClass: this, container: container}), - deferred; - - if (!this.recordArrays) { this.recordArrays = []; } - this.recordArrays.push(records); - - if (this._currentBatchIds) { - concatUnique(this._currentBatchIds, ids); - this._currentBatchRecordArrays.push(records); - } else { - this._currentBatchIds = concatUnique([], ids); - this._currentBatchRecordArrays = [records]; - } - - if (isFetch) { - deferred = Ember.Deferred.create(); - Ember.set(deferred, 'resolveWith', records); - - if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } - this._currentBatchDeferreds.push(deferred); - } - - Ember.run.scheduleOnce('data', this, this._executeBatch, container); - - return isFetch ? deferred : records; - }, - - findAll: function() { - return this._findFetchAll(false); - }, - - fetchAll: function() { - return this._findFetchAll(true); - }, - - _findFetchAll: function(isFetch, container) { - var self = this; - - var currentFetchPromise = this._currentFindFetchAllPromise; - if (isFetch && currentFetchPromise) { - return currentFetchPromise; - } else if (this._findAllRecordArray) { - if (isFetch) { - return new Ember.RSVP.Promise(function(resolve) { - resolve(self._findAllRecordArray); - }); - } else { - return this._findAllRecordArray; - } - } - - var records = this._findAllRecordArray = Ember.RecordArray.create({modelClass: this, container: container}); - - var promise = this._currentFindFetchAllPromise = this.adapter.findAll(this, records); - - promise.finally(function() { - self._currentFindFetchAllPromise = null; - }); - - // Remove the cached record array if the promise is rejected - if (promise.then) { - promise.then(null, function() { - self._findAllRecordArray = null; - return Ember.RSVP.reject.apply(null, arguments); - }); - } - - return isFetch ? promise : records; - }, - - findById: function(id) { - return this._findFetchById(id, false); - }, - - fetchById: function(id) { - return this._findFetchById(id, true); - }, - - _findFetchById: function(id, isFetch, container) { - var record = this.cachedRecordForId(id, container), - isLoaded = get(record, 'isLoaded'), - adapter = get(this, 'adapter'), - deferredOrPromise; - - if (isLoaded) { - if (isFetch) { - return new Ember.RSVP.Promise(function(resolve, reject) { - resolve(record); - }); - } else { - return record; - } - } - - deferredOrPromise = this._fetchById(record, id); - - return isFetch ? deferredOrPromise : record; - }, - - _currentBatchIds: null, - _currentBatchRecordArrays: null, - _currentBatchDeferreds: null, - - reload: function(id, container) { - var record = this.cachedRecordForId(id, container); - record.set('isLoaded', false); - return this._fetchById(record, id); - }, - - _fetchById: function(record, id) { - var adapter = get(this, 'adapter'), - deferred; - - if (adapter.findMany && !adapter.findMany.isUnimplemented) { - if (this._currentBatchIds) { - if (!contains(this._currentBatchIds, id)) { this._currentBatchIds.push(id); } - } else { - this._currentBatchIds = [id]; - this._currentBatchRecordArrays = []; - } - - deferred = Ember.Deferred.create(); - - //Attached the record to the deferred so we can resolve it later. - Ember.set(deferred, 'resolveWith', record); - - if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } - this._currentBatchDeferreds.push(deferred); - - Ember.run.scheduleOnce('data', this, this._executeBatch, record.container); - - return deferred; - } else { - return adapter.find(record, id); - } - }, - - _executeBatch: function(container) { - var batchIds = this._currentBatchIds, - batchRecordArrays = this._currentBatchRecordArrays, - batchDeferreds = this._currentBatchDeferreds, - self = this, - requestIds = [], - promise, - i; - - this._currentBatchIds = null; - this._currentBatchRecordArrays = null; - this._currentBatchDeferreds = null; - - for (i = 0; i < batchIds.length; i++) { - if (!this.cachedRecordForId(batchIds[i]).get('isLoaded')) { - requestIds.push(batchIds[i]); - } - } - - if (requestIds.length === 1) { - promise = get(this, 'adapter').find(this.cachedRecordForId(requestIds[0], container), requestIds[0]); - } else { - var recordArray = Ember.RecordArray.create({_ids: batchIds, container: container}); - if (requestIds.length === 0) { - promise = new Ember.RSVP.Promise(function(resolve, reject) { resolve(recordArray); }); - recordArray.notifyLoaded(); - } else { - promise = get(this, 'adapter').findMany(this, recordArray, requestIds); - } - } - - promise.then(function() { - for (var i = 0, l = batchRecordArrays.length; i < l; i++) { - batchRecordArrays[i].loadForFindMany(self); - } - - if (batchDeferreds) { - for (i = 0, l = batchDeferreds.length; i < l; i++) { - var resolveWith = Ember.get(batchDeferreds[i], 'resolveWith'); - batchDeferreds[i].resolve(resolveWith); - } - } - }).then(null, function(errorXHR) { - if (batchDeferreds) { - for (var i = 0, l = batchDeferreds.length; i < l; i++) { - batchDeferreds[i].reject(errorXHR); - } - } - }); - }, - - getCachedReferenceRecord: function(id, container){ - var ref = this._getReferenceById(id); - if(ref && ref.record) { - ref.record.container = container; - return ref.record; - } - return undefined; - }, - - cachedRecordForId: function(id, container) { - var record; - if (!this.transient) { - record = this.getCachedReferenceRecord(id, container); - } - - if (!record) { - var primaryKey = get(this, 'primaryKey'), - attrs = {isLoaded: false}; - - attrs[primaryKey] = id; - attrs.container = container; - record = this.create(attrs); - if (!this.transient) { - var sideloadedData = this.sideloadedData && this.sideloadedData[id]; - if (sideloadedData) { - record.load(id, sideloadedData); - } - } - } - - return record; - }, - - - addToRecordArrays: function(record) { - if (this._findAllRecordArray) { - this._findAllRecordArray.addObject(record); - } - if (this.recordArrays) { - this.recordArrays.forEach(function(recordArray) { - if (recordArray instanceof Ember.FilteredRecordArray) { - recordArray.registerObserversOnRecord(record); - recordArray.updateFilter(); - } else { - recordArray.addObject(record); - } - }); - } - }, - - unload: function (record) { - this.removeFromHasManyArrays(record); - this.removeFromRecordArrays(record); - var primaryKey = record.get(get(this, 'primaryKey')); - this.removeFromCache(primaryKey); - }, - - clearCache: function () { - this.sideloadedData = undefined; - this._referenceCache = undefined; - this._findAllRecordArray = undefined; - }, - - removeFromCache: function (key) { - if (this.sideloadedData && this.sideloadedData[key]) { - delete this.sideloadedData[key]; - } - if(this._referenceCache && this._referenceCache[key]) { - delete this._referenceCache[key]; - } - }, - - removeFromHasManyArrays: function(record) { - if (record._parentHasManyArrays) { - record._parentHasManyArrays.forEach(function(hasManyArray) { - hasManyArray.unloadObject(record); - }); - record._parentHasManyArrays = null; - } - }, - - removeFromRecordArrays: function(record) { - if (this._findAllRecordArray) { - this._findAllRecordArray.removeObject(record); - } - if (this.recordArrays) { - this.recordArrays.forEach(function(recordArray) { - recordArray.removeObject(record); - }); - } - }, - - // FIXME - findFromCacheOrLoad: function(data, container) { - var record; - if (!data[get(this, 'primaryKey')]) { - record = this.create({isLoaded: false, container: container}); - } else { - record = this.cachedRecordForId(data[get(this, 'primaryKey')], container); - } - // set(record, 'data', data); - record.load(data[get(this, 'primaryKey')], data); - return record; - }, - - registerRecordArray: function(recordArray) { - if (!this.recordArrays) { this.recordArrays = []; } - this.recordArrays.push(recordArray); - }, - - unregisterRecordArray: function(recordArray) { - if (!this.recordArrays) { return; } - Ember.A(this.recordArrays).removeObject(recordArray); - }, - - forEachCachedRecord: function(callback) { - if (!this._referenceCache) { return; } - var ids = Object.keys(this._referenceCache); - ids.map(function(id) { - return this._getReferenceById(id).record; - }, this).forEach(callback); - }, - - load: function(hashes) { - if (Ember.typeOf(hashes) !== 'array') { hashes = [hashes]; } - - if (!this.sideloadedData) { this.sideloadedData = {}; } - - for (var i = 0, l = hashes.length; i < l; i++) { - var hash = hashes[i], - primaryKey = hash[get(this, 'primaryKey')], - record = this.getCachedReferenceRecord(primaryKey); - - if (record) { - record.load(primaryKey, hash); - } else { - this.sideloadedData[primaryKey] = hash; - } - } - }, - - _getReferenceById: function(id) { - if (!this._referenceCache) { this._referenceCache = {}; } - return this._referenceCache[id]; - }, - - _getOrCreateReferenceForId: function(id) { - var reference = this._getReferenceById(id); - - if (!reference) { - reference = this._createReference(id); - } - - return reference; - }, - - _createReference: function(id) { - if (!this._referenceCache) { this._referenceCache = {}; } - - Ember.assert('The id ' + id + ' has already been used with another record of type ' + this.toString() + '.', !id || !this._referenceCache[id]); - - var reference = { - id: id, - clientId: this._clientIdCounter++ - }; - - this._cacheReference(reference); - - return reference; - }, - - _cacheReference: function(reference) { - if (!this._referenceCache) { this._referenceCache = {}; } - - // if we're creating an item, this process will be done - // later, once the object has been persisted. - if (!Ember.isEmpty(reference.id)) { - this._referenceCache[reference.id] = reference; - } - } -}); - - -})(); - -(function() { - -var get = Ember.get; - -function getType(record) { - var type = this.type; - - if (typeof this.type === "string" && this.type) { - this.type = Ember.get(Ember.lookup, this.type); - - if (!this.type) { - var store = record.container.lookup('store:main'); - this.type = store.modelFor(type); - this.type.reopenClass({ adapter: store.adapterFor(type) }); - } - } - - return this.type; -} - -Ember.hasMany = function(type, options) { - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany', getType: getType}; - - return Ember.computed(function(propertyKey, newContentArray, existingArray) { - type = meta.getType(this); - Ember.assert("Type cannot be empty", !Ember.isEmpty(type)); - - var key = options.key || propertyKey; - - if (arguments.length > 1) { - return existingArray.setObjects(newContentArray); - } else { - return this.getHasMany(key, type, meta, this.container); - } - }).property().meta(meta); -}; - -Ember.Model.reopen({ - getHasMany: function(key, type, meta, container) { - var embedded = meta.options.embedded, - collectionClass = embedded ? Ember.EmbeddedHasManyArray : Ember.HasManyArray; - - var collection = collectionClass.create({ - parent: this, - modelClass: type, - content: this._getHasManyContent(key, type, embedded), - embedded: embedded, - key: key, - relationshipKey: meta.relationshipKey, - container: container - }); - - this._registerHasManyArray(collection); - - return collection; - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set; - -function storeFor(record) { - if (record.container) { - return record.container.lookup('store:main'); - } - - return null; -} - -function getType(record) { - var type = this.type; - - if (typeof this.type === "string" && this.type) { - type = Ember.get(Ember.lookup, this.type); - - if (!type) { - var store = storeFor(record); - type = store.modelFor(this.type); - type.reopenClass({ adapter: store.adapterFor(this.type) }); - } - } - - return type; -} - -Ember.belongsTo = function(type, options) { - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo', getType: getType}; - - return Ember.computed(function(propertyKey, value, oldValue) { - type = meta.getType(this); - Ember.assert("Type cannot be empty.", !Ember.isEmpty(type)); - - var key = options.key || propertyKey; - - var dirtyAttributes = get(this, '_dirtyAttributes'), - createdDirtyAttributes = false, - self = this; - - var dirtyChanged = function(sender) { - if (sender.get('isDirty')) { - self._relationshipBecameDirty(key); - } else { - self._relationshipBecameClean(key); - } - }; - - if (!dirtyAttributes) { - dirtyAttributes = []; - createdDirtyAttributes = true; - } - - if (arguments.length > 1) { - - if (value) { - Ember.assert(Ember.String.fmt('Attempted to set property of type: %@ with a value of type: %@', - [value.constructor, type]), - value instanceof type); - } - - if (oldValue !== value) { - dirtyAttributes.pushObject(propertyKey); - } else { - dirtyAttributes.removeObject(propertyKey); - } - - if (createdDirtyAttributes) { - set(this, '_dirtyAttributes', dirtyAttributes); - } - - if (meta.options.embedded) { - if (oldValue) { - oldValue.removeObserver('isDirty', dirtyChanged); - } - if (value) { - value.addObserver('isDirty', dirtyChanged); - } - } - - return value === undefined ? null : value; - } else { - var store = storeFor(this); - value = this.getBelongsTo(key, type, meta, store); - this._registerBelongsTo(meta); - if (value !== null && meta.options.embedded) { - value.get('isDirty'); // getter must be called before adding observer - value.addObserver('isDirty', dirtyChanged); - } - return value; - } - }).property('_data').meta(meta); -}; - -Ember.Model.reopen({ - getBelongsTo: function(key, type, meta, store) { - var idOrAttrs = get(this, '_data.' + key), - record; - - if (Ember.isNone(idOrAttrs)) { - return null; - } - - if (meta.options.embedded) { - var primaryKey = get(type, 'primaryKey'), - id = idOrAttrs[primaryKey]; - record = type.create({ isLoaded: false, id: id, container: this.container }); - record.load(id, idOrAttrs); - } else { - if (store) { - record = store._findSync(meta.type, idOrAttrs); - } else { - record = type.find(idOrAttrs); - } - } - - return record; - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set, - meta = Ember.meta; - -Ember.Model.dataTypes = {}; - -Ember.Model.dataTypes[Date] = { - deserialize: function(string) { - if (!string) { return null; } - return new Date(string); - }, - serialize: function (date) { - if (!date) { return null; } - return date.toISOString(); - }, - isEqual: function(obj1, obj2) { - if (obj1 instanceof Date) { obj1 = this.serialize(obj1); } - if (obj2 instanceof Date) { obj2 = this.serialize(obj2); } - return obj1 === obj2; - } -}; - -Ember.Model.dataTypes[Number] = { - deserialize: function(string) { - if (!string && string !== 0) { return null; } - return Number(string); - }, - serialize: function (number) { - if (!number && number !== 0) { return null; } - return Number(number); - } -}; - -function deserialize(value, type) { - if (type && type.deserialize) { - return type.deserialize(value); - } else if (type && Ember.Model.dataTypes[type]) { - return Ember.Model.dataTypes[type].deserialize(value); - } else { - return value; - } -} - -function serialize(value, type) { - if (type && type.serialize) { - return type.serialize(value); - } else if (type && Ember.Model.dataTypes[type]) { - return Ember.Model.dataTypes[type].serialize(value); - } else { - return value; - } -} - -Ember.attr = function(type, options) { - return Ember.computed(function(key, value) { - var data = get(this, '_data'), - dataKey = this.dataKey(key), - dataValue = data && get(data, dataKey), - beingCreated = meta(this).proto === this, - dirtyAttributes = get(this, '_dirtyAttributes'), - createdDirtyAttributes = false; - - if (!dirtyAttributes) { - dirtyAttributes = []; - createdDirtyAttributes = true; - } - - if (arguments.length === 2) { - if (beingCreated) { - if (!data) { - data = {}; - set(this, '_data', data); - } - dataValue = data[dataKey] = value; - } - - if (dataValue !== serialize(value, type)) { - dirtyAttributes.pushObject(key); - } else { - dirtyAttributes.removeObject(key); - } - - if (createdDirtyAttributes) { - set(this, '_dirtyAttributes', dirtyAttributes); - } - - return value; - } - - if (dataValue==null && options && options.defaultValue!=null) { - return Ember.copy(options.defaultValue); - } - - return this.getAttr(key, deserialize(dataValue, type)); - }).property('_data').meta({isAttribute: true, type: type, options: options}); -}; - - -})(); - -(function() { - -var get = Ember.get; - -Ember.RESTAdapter = Ember.Adapter.extend({ - find: function(record, id) { - var url = this.buildURL(record.constructor, id), - self = this; - - return this.ajax(url).then(function(data) { - self.didFind(record, id, data); - return record; - }); - }, - - didFind: function(record, id, data) { - var rootKey = get(record.constructor, 'rootKey'), - dataToLoad = rootKey ? get(data, rootKey) : data; - - record.load(id, dataToLoad); - }, - - findAll: function(klass, records) { - var url = this.buildURL(klass), - self = this; - - return this.ajax(url).then(function(data) { - self.didFindAll(klass, records, data); - return records; - }); - }, - - didFindAll: function(klass, records, data) { - var collectionKey = get(klass, 'collectionKey'), - dataToLoad = collectionKey ? get(data, collectionKey) : data; - - records.load(klass, dataToLoad); - }, - - findQuery: function(klass, records, params) { - var url = this.buildURL(klass), - self = this; - - return this.ajax(url, params).then(function(data) { - self.didFindQuery(klass, records, params, data); - return records; - }); - }, - - didFindQuery: function(klass, records, params, data) { - var collectionKey = get(klass, 'collectionKey'), - dataToLoad = collectionKey ? get(data, collectionKey) : data; - - records.load(klass, dataToLoad); - }, - - createRecord: function(record) { - var url = this.buildURL(record.constructor), - self = this; - - return this.ajax(url, record.toJSON(), "POST").then(function(data) { - self.didCreateRecord(record, data); - return record; - }); - }, - - didCreateRecord: function(record, data) { - this._loadRecordFromData(record, data); - record.didCreateRecord(); - }, - - saveRecord: function(record) { - var primaryKey = get(record.constructor, 'primaryKey'), - url = this.buildURL(record.constructor, get(record, primaryKey)), - self = this; - - return this.ajax(url, record.toJSON(), "PUT").then(function(data) { // TODO: Some APIs may or may not return data - self.didSaveRecord(record, data); - return record; - }); - }, - - didSaveRecord: function(record, data) { - this._loadRecordFromData(record, data); - record.didSaveRecord(); - }, - - deleteRecord: function(record) { - var primaryKey = get(record.constructor, 'primaryKey'), - url = this.buildURL(record.constructor, get(record, primaryKey)), - self = this; - - return this.ajax(url, record.toJSON(), "DELETE").then(function(data) { // TODO: Some APIs may or may not return data - self.didDeleteRecord(record, data); - }); - }, - - didDeleteRecord: function(record, data) { - record.didDeleteRecord(); - }, - - ajax: function(url, params, method, settings) { - return this._ajax(url, params, (method || "GET"), settings); - }, - - buildURL: function(klass, id) { - var urlRoot = get(klass, 'url'); - var urlSuffix = get(klass, 'urlSuffix') || ''; - if (!urlRoot) { throw new Error('Ember.RESTAdapter requires a `url` property to be specified'); } - - if (!Ember.isEmpty(id)) { - return urlRoot + "/" + id + urlSuffix; - } else { - return urlRoot + urlSuffix; - } - }, - - ajaxSettings: function(url, method) { - return { - url: url, - type: method, - dataType: "json" - }; - }, - - _ajax: function(url, params, method, settings) { - if (!settings) { - settings = this.ajaxSettings(url, method); - } - - return new Ember.RSVP.Promise(function(resolve, reject) { - if (params) { - if (method === "GET") { - settings.data = params; - } else { - settings.contentType = "application/json; charset=utf-8"; - settings.data = JSON.stringify(params); - } - } - - settings.success = function(json) { - Ember.run(null, resolve, json); - }; - - settings.error = function(jqXHR, textStatus, errorThrown) { - // https://github.com/ebryn/ember-model/issues/202 - if (jqXHR && typeof jqXHR === 'object') { - jqXHR.then = null; - } - - Ember.run(null, reject, jqXHR); - }; - - - Ember.$.ajax(settings); - }); - }, - - _loadRecordFromData: function(record, data) { - var rootKey = get(record.constructor, 'rootKey'), - primaryKey = get(record.constructor, 'primaryKey'); - // handle HEAD response where no data is provided by server - if (data) { - data = rootKey ? get(data, rootKey) : data; - if (!Ember.isEmpty(data)) { - record.load(data[primaryKey], data); - } - } - } -}); - - -})(); - -(function() { - -var get = Ember.get; - -Ember.LoadPromise = Ember.Object.extend(Ember.DeferredMixin, { - init: function() { - this._super.apply(this, arguments); - - var target = get(this, 'target'); - - if (get(target, 'isLoaded') && !get(target, 'isNew')) { - this.resolve(target); - } else { - target.one('didLoad', this, function() { - this.resolve(target); - }); - } - } -}); - -Ember.loadPromise = function(target) { - if (Ember.isNone(target)) { - return null; - } else if (target.then) { - return target; - } else { - return Ember.LoadPromise.create({target: target}); - } -}; - - -})(); - -(function() { - -// This is a debug adapter for the Ember Extension, don't let the fact this is called an "adapter" confuse you. -// Most copied from: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/debug/debug_adapter.js - -if (!Ember.DataAdapter) { return; } - -var get = Ember.get, capitalize = Ember.String.capitalize, underscore = Ember.String.underscore; - -var DebugAdapter = Ember.DataAdapter.extend({ - getFilters: function() { - return [ - { name: 'isNew', desc: 'New' }, - { name: 'isModified', desc: 'Modified' }, - { name: 'isClean', desc: 'Clean' } - ]; - }, - - detect: function(klass) { - return klass !== Ember.Model && Ember.Model.detect(klass); - }, - - columnsForType: function(type) { - var columns = [], count = 0, self = this; - type.getAttributes().forEach(function(name, meta) { - if (count++ > self.attributeLimit) { return false; } - var desc = capitalize(underscore(name).replace('_', ' ')); - columns.push({ name: name, desc: desc }); - }); - return columns; - }, - - getRecords: function(type) { - var records = []; - type.forEachCachedRecord(function(record) { records.push(record); }); - return records; - }, - - getRecordColumnValues: function(record) { - var self = this, count = 0, - columnValues = { id: get(record, 'id') }; - - record.constructor.getAttributes().forEach(function(key) { - if (count++ > self.attributeLimit) { - return false; - } - var value = get(record, key); - columnValues[key] = value; - }); - return columnValues; - }, - - getRecordKeywords: function(record) { - var keywords = [], keys = Ember.A(['id']); - record.constructor.getAttributes().forEach(function(key) { - keys.push(key); - }); - keys.forEach(function(key) { - keywords.push(get(record, key)); - }); - return keywords; - }, - - getRecordFilterValues: function(record) { - return { - isNew: record.get('isNew'), - isModified: record.get('isDirty') && !record.get('isNew'), - isClean: !record.get('isDirty') - }; - }, - - getRecordColor: function(record) { - var color = 'black'; - if (record.get('isNew')) { - color = 'green'; - } else if (record.get('isDirty')) { - color = 'blue'; - } - return color; - }, - - observeRecord: function(record, recordUpdated) { - var releaseMethods = Ember.A(), self = this, - keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); - - record.constructor.getAttributes().forEach(function(key) { - keysToObserve.push(key); - }); - - keysToObserve.forEach(function(key) { - var handler = function() { - recordUpdated(self.wrapRecord(record)); - }; - Ember.addObserver(record, key, handler); - releaseMethods.push(function() { - Ember.removeObserver(record, key, handler); - }); - }); - - var release = function() { - releaseMethods.forEach(function(fn) { fn(); } ); - }; - - return release; - } -}); - -Ember.onLoad('Ember.Application', function(Application) { - Application.initializer({ - name: "data-adapter", - - initialize: function(container, application) { - application.register('data-adapter:main', DebugAdapter); - } - }); -}); - - -})(); - -(function() { - -function NIL() {} - -Ember.Model.Store = Ember.Object.extend({ - container: null, - - modelFor: function(type) { - return this.container.lookupFactory('model:'+type); - }, - - adapterFor: function(type) { - var adapter = this.modelFor(type).adapter, - container = this.container; - - if (adapter && adapter !== Ember.Model.adapter) { - return adapter; - } else { - adapter = container.lookupFactory('adapter:'+ type) || - container.lookupFactory('adapter:application') || - container.lookupFactory('adapter:REST'); - - return adapter ? adapter.create() : adapter; - } - }, - - createRecord: function(type) { - var klass = this.modelFor(type); - klass.reopenClass({adapter: this.adapterFor(type)}); - return klass.create({container: this.container}); - }, - - find: function(type, id) { - if (arguments.length === 1) { id = NIL; } - return this._find(type, id, true); - }, - - _find: function(type, id, async) { - var klass = this.modelFor(type); - - // if (!klass.adapter) { - klass.reopenClass({adapter: this.adapterFor(type)}); - // } - - if (id === NIL) { - return klass._findFetchAll(async, this.container); - } else if (Ember.isArray(id)) { - return klass._findFetchMany(id, async, this.container); - } else if (typeof id === 'object') { - return klass._findFetchQuery(id, async, this.container); - } else { - return klass._findFetchById(id, async, this.container); - } - }, - - _findSync: function(type, id) { - return this._find(type, id, false); - } -}); - -Ember.onLoad('Ember.Application', function(Application) { - Application.initializer({ - name: "store", - - initialize: function(container, application) { - application.register('store:main', container.lookupFactory('store:application') || Ember.Model.Store); - - application.inject('route', 'store', 'store:main'); - application.inject('controller', 'store', 'store:main'); - } - }); -}); - - -})(); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-model/ember-model.prod.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-model/ember-model.prod.js deleted file mode 100644 index cca300d93..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-model/ember-model.prod.js +++ /dev/null @@ -1,2044 +0,0 @@ -// ========================================================================== -// Project: Ember Model -// Copyright: ©2013 Erik Bryn and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== - -// 0.0.11-98-g81e0b74 -// 81e0b74 (2014-06-27 14:13:56 -0700) - -(function() { - -var VERSION = '0.0.11'; - -if (Ember.libraries) { - Ember.libraries.register('Ember Model', VERSION); -} - - -})(); - -(function() { - -function mustImplement(message) { - var fn = function() { - var className = this.constructor.toString(); - - throw new Error(message.replace('{{className}}', className)); - }; - fn.isUnimplemented = true; - return fn; -} - -Ember.Adapter = Ember.Object.extend({ - find: mustImplement('{{className}} must implement find'), - findQuery: mustImplement('{{className}} must implement findQuery'), - findMany: mustImplement('{{className}} must implement findMany'), - findAll: mustImplement('{{className}} must implement findAll'), - createRecord: mustImplement('{{className}} must implement createRecord'), - saveRecord: mustImplement('{{className}} must implement saveRecord'), - deleteRecord: mustImplement('{{className}} must implement deleteRecord'), - - load: function(record, id, data) { - record.load(id, data); - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set; - -Ember.FixtureAdapter = Ember.Adapter.extend({ - _counter: 0, - _findData: function(klass, id) { - var fixtures = klass.FIXTURES, - idAsString = id.toString(), - primaryKey = get(klass, 'primaryKey'), - data = Ember.A(fixtures).find(function(el) { return (el[primaryKey]).toString() === idAsString; }); - - return data; - }, - - _setPrimaryKey: function(record) { - var klass = record.constructor, - fixtures = klass.FIXTURES, - primaryKey = get(klass, 'primaryKey'); - - - if(record.get(primaryKey)) { - return; - } - - set(record, primaryKey, this._generatePrimaryKey()); - }, - - _generatePrimaryKey: function() { - var counter = this.get("_counter"); - - this.set("_counter", counter + 1); - - return "fixture-" + counter; - }, - - find: function(record, id) { - var data = this._findData(record.constructor, id); - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - Ember.run(record, record.load, id, data); - resolve(record); - }, 0); - }); - }, - - findMany: function(klass, records, ids) { - var fixtures = klass.FIXTURES, - requestedData = []; - - for (var i = 0, l = ids.length; i < l; i++) { - requestedData.push(this._findData(klass, ids[i])); - } - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - Ember.run(records, records.load, klass, requestedData); - resolve(records); - }, 0); - }); - }, - - findAll: function(klass, records) { - var fixtures = klass.FIXTURES; - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - Ember.run(records, records.load, klass, fixtures); - resolve(records); - }, 0); - }); - }, - - createRecord: function(record) { - var klass = record.constructor, - fixtures = klass.FIXTURES, - self = this; - - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - self._setPrimaryKey(record); - fixtures.push(klass.findFromCacheOrLoad(record.toJSON())); - record.didCreateRecord(); - resolve(record); - }, 0); - }); - }, - - saveRecord: function(record) { - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - record.didSaveRecord(); - resolve(record); - }, 0); - }); - }, - - deleteRecord: function(record) { - return new Ember.RSVP.Promise(function(resolve, reject) { - Ember.run.later(this, function() { - record.didDeleteRecord(); - resolve(record); - }, 0); - }); - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set; - -Ember.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, { - isLoaded: false, - isLoading: Ember.computed.not('isLoaded'), - - load: function(klass, data) { - set(this, 'content', this.materializeData(klass, data)); - this.notifyLoaded(); - }, - - loadForFindMany: function(klass) { - var self = this; - var content = get(this, '_ids').map(function(id) { return klass.cachedRecordForId(id, self.container); }); - set(this, 'content', Ember.A(content)); - this.notifyLoaded(); - }, - - notifyLoaded: function() { - set(this, 'isLoaded', true); - this.trigger('didLoad'); - }, - - materializeData: function(klass, data) { - var self = this; - return Ember.A(data.map(function(el) { - return klass.findFromCacheOrLoad(el, self.container); // FIXME - })); - }, - - reload: function() { - var modelClass = this.get('modelClass'), - self = this, - promises; - - set(this, 'isLoaded', false); - if (modelClass._findAllRecordArray === this) { - return modelClass.adapter.findAll(modelClass, this); - } else if (this._query) { - return modelClass.adapter.findQuery(modelClass, this, this._query); - } else { - promises = this.map(function(record) { - return record.reload(); - }); - return Ember.RSVP.all(promises).then(function(data) { - self.notifyLoaded(); - }); - } - } -}); - - -})(); - -(function() { - -var get = Ember.get; - -Ember.FilteredRecordArray = Ember.RecordArray.extend({ - init: function() { - if (!get(this, 'modelClass')) { - throw new Error('FilteredRecordArrays must be created with a modelClass'); - } - if (!get(this, 'filterFunction')) { - throw new Error('FilteredRecordArrays must be created with a filterFunction'); - } - if (!get(this, 'filterProperties')) { - throw new Error('FilteredRecordArrays must be created with filterProperties'); - } - - var modelClass = get(this, 'modelClass'); - modelClass.registerRecordArray(this); - - this.registerObservers(); - this.updateFilter(); - - this._super(); - }, - - updateFilter: function() { - var self = this, - results = []; - get(this, 'modelClass').forEachCachedRecord(function(record) { - if (self.filterFunction(record)) { - results.push(record); - } - }); - this.set('content', Ember.A(results)); - }, - - updateFilterForRecord: function(record) { - var results = get(this, 'content'); - if (this.filterFunction(record) && !results.contains(record)) { - results.pushObject(record); - } - }, - - registerObservers: function() { - var self = this; - get(this, 'modelClass').forEachCachedRecord(function(record) { - self.registerObserversOnRecord(record); - }); - }, - - registerObserversOnRecord: function(record) { - var self = this, - filterProperties = get(this, 'filterProperties'); - - for (var i = 0, l = get(filterProperties, 'length'); i < l; i++) { - record.addObserver(filterProperties[i], self, 'updateFilterForRecord'); - } - } -}); - -})(); - -(function() { - -var get = Ember.get, set = Ember.set; - -Ember.ManyArray = Ember.RecordArray.extend({ - _records: null, - originalContent: null, - _modifiedRecords: null, - - unloadObject: function(record) { - var obj = get(this, 'content').findBy('clientId', record._reference.clientId); - get(this, 'content').removeObject(obj); - - var originalObj = get(this, 'originalContent').findBy('clientId', record._reference.clientId); - get(this, 'originalContent').removeObject(originalObj); - }, - - isDirty: function() { - var originalContent = get(this, 'originalContent'), - originalContentLength = get(originalContent, 'length'), - content = get(this, 'content'), - contentLength = get(content, 'length'); - - if (originalContentLength !== contentLength) { return true; } - - if (this._modifiedRecords && this._modifiedRecords.length) { return true; } - - var isDirty = false; - - for (var i = 0, l = contentLength; i < l; i++) { - if (!originalContent.contains(content[i])) { - isDirty = true; - break; - } - } - - return isDirty; - }.property('content.[]', 'originalContent.[]', '_modifiedRecords.[]'), - - objectAtContent: function(idx) { - var content = get(this, 'content'); - - if (!content.length) { return; } - - // need to add observer if it wasn't materialized before - var observerNeeded = (content[idx].record) ? false : true; - - var record = this.materializeRecord(idx, this.container); - - if (observerNeeded) { - var isDirtyRecord = record.get('isDirty'), isNewRecord = record.get('isNew'); - if (isDirtyRecord || isNewRecord) { this._modifiedRecords.pushObject(content[idx]); } - Ember.addObserver(content[idx], 'record.isDirty', this, 'recordStateChanged'); - record.registerParentHasManyArray(this); - } - - return record; - }, - - save: function() { - // TODO: loop over dirty records only - return Ember.RSVP.all(this.map(function(record) { - return record.save(); - })); - }, - - replaceContent: function(index, removed, added) { - added = Ember.EnumerableUtils.map(added, function(record) { - return record._reference; - }, this); - - this._super(index, removed, added); - }, - - _contentWillChange: function() { - var content = get(this, 'content'); - - if (content) { - this.arrayWillChange(content, 0, get(content, 'length'), 0); - content.removeArrayObserver(this); - this._setupOriginalContent(content); - } - }.observesBefore('content'), - - _contentDidChange: function() { - var content = get(this, 'content'); - if (content) { - content.addArrayObserver(this); - this.arrayDidChange(content, 0, 0, get(content, 'length')); - } - }.observes('content'), - - arrayWillChange: function(item, idx, removedCnt, addedCnt) { - var content = item; - for (var i = idx; i < idx+removedCnt; i++) { - var currentItem = content[i]; - if (currentItem && currentItem.record) { - this._modifiedRecords.removeObject(currentItem); - currentItem.record.unregisterParentHasManyArray(this); - Ember.removeObserver(currentItem, 'record.isDirty', this, 'recordStateChanged'); - } - } - }, - - arrayDidChange: function(item, idx, removedCnt, addedCnt) { - var parent = get(this, 'parent'), relationshipKey = get(this, 'relationshipKey'), - isDirty = get(this, 'isDirty'); - - var content = item; - for (var i = idx; i < idx+addedCnt; i++) { - var currentItem = content[i]; - if (currentItem && currentItem.record) { - var isDirtyRecord = currentItem.record.get('isDirty'), isNewRecord = currentItem.record.get('isNew'); // why newly created object is not dirty? - if (isDirtyRecord || isNewRecord) { this._modifiedRecords.pushObject(currentItem); } - Ember.addObserver(currentItem, 'record.isDirty', this, 'recordStateChanged'); - currentItem.record.registerParentHasManyArray(this); - } - } - - if (isDirty) { - parent._relationshipBecameDirty(relationshipKey); - } else { - parent._relationshipBecameClean(relationshipKey); - } - }, - - load: function(content) { - Ember.setProperties(this, { - content: content, - originalContent: content.slice() - }); - set(this, '_modifiedRecords', []); - }, - - revert: function() { - this._setupOriginalContent(); - }, - - _setupOriginalContent: function(content) { - content = content || get(this, 'content'); - if (content) { - set(this, 'originalContent', content.slice()); - } - set(this, '_modifiedRecords', []); - }, - - init: function() { - this._super(); - this._setupOriginalContent(); - this._contentDidChange(); - }, - - recordStateChanged: function(obj, keyName) { - var parent = get(this, 'parent'), relationshipKey = get(this, 'relationshipKey'); - - if (obj.record.get('isDirty')) { - if (this._modifiedRecords.indexOf(obj) === -1) { this._modifiedRecords.pushObject(obj); } - parent._relationshipBecameDirty(relationshipKey); - } else { - if (this._modifiedRecords.indexOf(obj) > -1) { this._modifiedRecords.removeObject(obj); } - if (!this.get('isDirty')) { - parent._relationshipBecameClean(relationshipKey); - } - } - } -}); - -Ember.HasManyArray = Ember.ManyArray.extend({ - materializeRecord: function(idx, container) { - var klass = get(this, 'modelClass'), - content = get(this, 'content'), - reference = content.objectAt(idx), - record; - - if (reference.record) { - record = reference.record; - } else { - record = klass.find(reference.id); - } - - record.container = container; - return record; - }, - - toJSON: function() { - var ids = [], content = this.get('content'); - - content.forEach(function(reference) { - if (reference.id) { - ids.push(reference.id); - } - }); - - return ids; - } -}); - -Ember.EmbeddedHasManyArray = Ember.ManyArray.extend({ - create: function(attrs) { - var klass = get(this, 'modelClass'), - record = klass.create(attrs); - - this.pushObject(record); - - return record; // FIXME: inject parent's id - }, - - materializeRecord: function(idx, container) { - var klass = get(this, 'modelClass'), - primaryKey = get(klass, 'primaryKey'), - content = get(this, 'content'), - reference = content.objectAt(idx), - attrs = reference.data; - - var record; - if (reference.record) { - record = reference.record; - } else { - record = klass.create({ _reference: reference, container: container }); - reference.record = record; - if (attrs) { - record.load(attrs[primaryKey], attrs); - } - } - - record.container = container; - return record; - }, - - toJSON: function() { - return this.map(function(record) { - return record.toJSON(); - }); - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set, - setProperties = Ember.setProperties, - meta = Ember.meta, - underscore = Ember.String.underscore; - -function contains(array, element) { - for (var i = 0, l = array.length; i < l; i++) { - if (array[i] === element) { return true; } - } - return false; -} - -function concatUnique(toArray, fromArray) { - var e; - for (var i = 0, l = fromArray.length; i < l; i++) { - e = fromArray[i]; - if (!contains(toArray, e)) { toArray.push(e); } - } - return toArray; -} - -function hasCachedValue(object, key) { - var objectMeta = meta(object, false); - if (objectMeta) { - return key in objectMeta.cache; - } -} - -Ember.run.queues.push('data'); - -Ember.Model = Ember.Object.extend(Ember.Evented, { - isLoaded: true, - isLoading: Ember.computed.not('isLoaded'), - isNew: true, - isDeleted: false, - _dirtyAttributes: null, - - /** - Called when attribute is accessed. - - @method getAttr - @param key {String} key which is being accessed - @param value {Object} value, which will be returned from getter by default - */ - getAttr: function(key, value) { - return value; - }, - - isDirty: function() { - var dirtyAttributes = get(this, '_dirtyAttributes'); - return dirtyAttributes && dirtyAttributes.length !== 0 || false; - }.property('_dirtyAttributes.length'), - - _relationshipBecameDirty: function(name) { - var dirtyAttributes = get(this, '_dirtyAttributes'); - if (!dirtyAttributes.contains(name)) { dirtyAttributes.pushObject(name); } - }, - - _relationshipBecameClean: function(name) { - var dirtyAttributes = get(this, '_dirtyAttributes'); - dirtyAttributes.removeObject(name); - }, - - dataKey: function(key) { - var camelizeKeys = get(this.constructor, 'camelizeKeys'); - var meta = this.constructor.metaForProperty(key); - if (meta.options && meta.options.key) { - return camelizeKeys ? underscore(meta.options.key) : meta.options.key; - } - return camelizeKeys ? underscore(key) : key; - }, - - init: function() { - this._createReference(); - if (!this._dirtyAttributes) { - set(this, '_dirtyAttributes', []); - } - this._super(); - }, - - _createReference: function() { - var reference = this._reference, - id = this.getPrimaryKey(); - - if (!reference) { - reference = this.constructor._getOrCreateReferenceForId(id); - reference.record = this; - this._reference = reference; - } else if (reference.id !== id) { - reference.id = id; - this.constructor._cacheReference(reference); - } - - if (!reference.id) { - reference.id = id; - } - - return reference; - }, - - getPrimaryKey: function() { - return get(this, get(this.constructor, 'primaryKey')); - }, - - load: function(id, hash) { - var data = {}; - data[get(this.constructor, 'primaryKey')] = id; - set(this, '_data', Ember.merge(data, hash)); - this.getWithDefault('_dirtyAttributes', []).clear(); - - this._reloadHasManys(); - - // eagerly load embedded data - var relationships = this.constructor._relationships || [], meta = Ember.meta(this), relationshipKey, relationship, relationshipMeta, relationshipData, relationshipType; - for (var i = 0, l = relationships.length; i < l; i++) { - relationshipKey = relationships[i]; - relationship = meta.descs[relationshipKey]; - relationshipMeta = relationship.meta(); - - if (relationshipMeta.options.embedded) { - relationshipType = relationshipMeta.type; - if (typeof relationshipType === "string") { - relationshipType = Ember.get(Ember.lookup, relationshipType) || this.container.lookupFactory('model:'+ relationshipType); - } - - relationshipData = data[relationshipKey]; - if (relationshipData) { - relationshipType.load(relationshipData); - } - } - } - - set(this, 'isNew', false); - set(this, 'isLoaded', true); - this._createReference(); - this.trigger('didLoad'); - }, - - didDefineProperty: function(proto, key, value) { - if (value instanceof Ember.Descriptor) { - var meta = value.meta(); - var klass = proto.constructor; - - if (meta.isAttribute) { - if (!klass._attributes) { klass._attributes = []; } - klass._attributes.push(key); - } else if (meta.isRelationship) { - if (!klass._relationships) { klass._relationships = []; } - klass._relationships.push(key); - meta.relationshipKey = key; - } - } - }, - - serializeHasMany: function(key, meta) { - return this.get(key).toJSON(); - }, - - serializeBelongsTo: function(key, meta) { - if (meta.options.embedded) { - var record = this.get(key); - return record ? record.toJSON() : null; - } else { - var primaryKey = get(meta.getType(), 'primaryKey'); - return this.get(key + '.' + primaryKey); - } - }, - - toJSON: function() { - var key, meta, - json = {}, - attributes = this.constructor.getAttributes(), - relationships = this.constructor.getRelationships(), - properties = attributes ? this.getProperties(attributes) : {}, - rootKey = get(this.constructor, 'rootKey'); - - for (key in properties) { - meta = this.constructor.metaForProperty(key); - if (meta.type && meta.type.serialize) { - json[this.dataKey(key)] = meta.type.serialize(properties[key]); - } else if (meta.type && Ember.Model.dataTypes[meta.type]) { - json[this.dataKey(key)] = Ember.Model.dataTypes[meta.type].serialize(properties[key]); - } else { - json[this.dataKey(key)] = properties[key]; - } - } - - if (relationships) { - var data, relationshipKey; - - for(var i = 0; i < relationships.length; i++) { - key = relationships[i]; - meta = this.constructor.metaForProperty(key); - relationshipKey = meta.options.key || key; - - if (meta.kind === 'belongsTo') { - data = this.serializeBelongsTo(key, meta); - } else { - data = this.serializeHasMany(key, meta); - } - - json[relationshipKey] = data; - - } - } - - if (rootKey) { - var jsonRoot = {}; - jsonRoot[rootKey] = json; - return jsonRoot; - } else { - return json; - } - }, - - save: function() { - var adapter = this.constructor.adapter; - set(this, 'isSaving', true); - if (get(this, 'isNew')) { - return adapter.createRecord(this); - } else if (get(this, 'isDirty')) { - return adapter.saveRecord(this); - } else { // noop, return a resolved promise - var self = this, - promise = new Ember.RSVP.Promise(function(resolve, reject) { - resolve(self); - }); - set(this, 'isSaving', false); - return promise; - } - }, - - reload: function() { - this.getWithDefault('_dirtyAttributes', []).clear(); - return this.constructor.reload(this.get(get(this.constructor, 'primaryKey')), this.container); - }, - - revert: function() { - this.getWithDefault('_dirtyAttributes', []).clear(); - this.notifyPropertyChange('_data'); - this._reloadHasManys(true); - }, - - didCreateRecord: function() { - var primaryKey = get(this.constructor, 'primaryKey'), - id = get(this, primaryKey); - - set(this, 'isNew', false); - - set(this, '_dirtyAttributes', []); - this.constructor.addToRecordArrays(this); - this.trigger('didCreateRecord'); - this.didSaveRecord(); - }, - - didSaveRecord: function() { - set(this, 'isSaving', false); - this.trigger('didSaveRecord'); - if (this.get('isDirty')) { this._copyDirtyAttributesToData(); } - }, - - deleteRecord: function() { - return this.constructor.adapter.deleteRecord(this); - }, - - didDeleteRecord: function() { - this.constructor.removeFromRecordArrays(this); - set(this, 'isDeleted', true); - this.trigger('didDeleteRecord'); - }, - - _copyDirtyAttributesToData: function() { - if (!this._dirtyAttributes) { return; } - var dirtyAttributes = this._dirtyAttributes, - data = get(this, '_data'), - key; - - if (!data) { - data = {}; - set(this, '_data', data); - } - for (var i = 0, l = dirtyAttributes.length; i < l; i++) { - // TODO: merge Object.create'd object into prototype - key = dirtyAttributes[i]; - data[this.dataKey(key)] = this.cacheFor(key); - } - set(this, '_dirtyAttributes', []); - this._resetDirtyStateInNestedObjects(this); // we need to reset isDirty state to all child objects in embedded relationships - }, - - _resetDirtyStateInNestedObjects: function(object) { - var i, obj; - if (object._hasManyArrays) { - for (i = 0; i < object._hasManyArrays.length; i++) { - var array = object._hasManyArrays[i]; - array.revert(); - if (array.embedded) { - for (var j = 0; j < array.get('length'); j++) { - obj = array.objectAt(j); - obj._copyDirtyAttributesToData(); - } - } - } - } - - if (object._belongsTo) { - for (i = 0; i < object._belongsTo.length; i++) { - var belongsTo = object._belongsTo[i]; - if (belongsTo.options.embedded) { - obj = this.get(belongsTo.relationshipKey); - if (obj) { - obj._copyDirtyAttributesToData(); - } - } - } - } - }, - - _registerHasManyArray: function(array) { - if (!this._hasManyArrays) { this._hasManyArrays = Ember.A([]); } - - this._hasManyArrays.pushObject(array); - }, - - registerParentHasManyArray: function(array) { - if (!this._parentHasManyArrays) { this._parentHasManyArrays = Ember.A([]); } - - this._parentHasManyArrays.pushObject(array); - }, - - unregisterParentHasManyArray: function(array) { - if (!this._parentHasManyArrays) { return; } - - this._parentHasManyArrays.removeObject(array); - }, - - _reloadHasManys: function(reverting) { - if (!this._hasManyArrays) { return; } - var i, j; - for (i = 0; i < this._hasManyArrays.length; i++) { - var array = this._hasManyArrays[i], - hasManyContent = this._getHasManyContent(get(array, 'key'), get(array, 'modelClass'), get(array, 'embedded')); - if (!reverting) { - for (j = 0; j < array.get('length'); j++) { - if (array.objectAt(j).get('isNew') && !array.objectAt(j).get('isDeleted')) { - hasManyContent.addObject(array.objectAt(j)._reference); - } - } - } - array.load(hasManyContent); - } - }, - - _getHasManyContent: function(key, type, embedded) { - var content = get(this, '_data.' + key); - - if (content) { - var mapFunction, primaryKey, reference; - if (embedded) { - primaryKey = get(type, 'primaryKey'); - mapFunction = function(attrs) { - reference = type._getOrCreateReferenceForId(attrs[primaryKey]); - reference.data = attrs; - return reference; - }; - } else { - mapFunction = function(id) { return type._getOrCreateReferenceForId(id); }; - } - content = Ember.EnumerableUtils.map(content, mapFunction); - } - - return Ember.A(content || []); - }, - - _registerBelongsTo: function(key) { - if (!this._belongsTo) { this._belongsTo = Ember.A([]); } - - this._belongsTo.pushObject(key); - } -}); - -Ember.Model.reopenClass({ - primaryKey: 'id', - - adapter: Ember.Adapter.create(), - - _clientIdCounter: 1, - - getAttributes: function() { - this.proto(); // force class "compilation" if it hasn't been done. - var attributes = this._attributes || []; - if (typeof this.superclass.getAttributes === 'function') { - attributes = this.superclass.getAttributes().concat(attributes); - } - return attributes; - }, - - getRelationships: function() { - this.proto(); // force class "compilation" if it hasn't been done. - var relationships = this._relationships || []; - if (typeof this.superclass.getRelationships === 'function') { - relationships = this.superclass.getRelationships().concat(relationships); - } - return relationships; - }, - - fetch: function(id) { - if (!arguments.length) { - return this._findFetchAll(true); - } else if (Ember.isArray(id)) { - return this._findFetchMany(id, true); - } else if (typeof id === 'object') { - return this._findFetchQuery(id, true); - } else { - return this._findFetchById(id, true); - } - }, - - find: function(id) { - if (!arguments.length) { - return this._findFetchAll(false); - } else if (Ember.isArray(id)) { - return this._findFetchMany(id, false); - } else if (typeof id === 'object') { - return this._findFetchQuery(id, false); - } else { - return this._findFetchById(id, false); - } - }, - - findQuery: function(params) { - return this._findFetchQuery(params, false); - }, - - fetchQuery: function(params) { - return this._findFetchQuery(params, true); - }, - - _findFetchQuery: function(params, isFetch, container) { - var records = Ember.RecordArray.create({modelClass: this, _query: params, container: container}); - - var promise = this.adapter.findQuery(this, records, params); - - return isFetch ? promise : records; - }, - - findMany: function(ids) { - return this._findFetchMany(ids, false); - }, - - fetchMany: function(ids) { - return this._findFetchMany(ids, true); - }, - - _findFetchMany: function(ids, isFetch, container) { - 0; - - var records = Ember.RecordArray.create({_ids: ids, modelClass: this, container: container}), - deferred; - - if (!this.recordArrays) { this.recordArrays = []; } - this.recordArrays.push(records); - - if (this._currentBatchIds) { - concatUnique(this._currentBatchIds, ids); - this._currentBatchRecordArrays.push(records); - } else { - this._currentBatchIds = concatUnique([], ids); - this._currentBatchRecordArrays = [records]; - } - - if (isFetch) { - deferred = Ember.Deferred.create(); - Ember.set(deferred, 'resolveWith', records); - - if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } - this._currentBatchDeferreds.push(deferred); - } - - Ember.run.scheduleOnce('data', this, this._executeBatch, container); - - return isFetch ? deferred : records; - }, - - findAll: function() { - return this._findFetchAll(false); - }, - - fetchAll: function() { - return this._findFetchAll(true); - }, - - _findFetchAll: function(isFetch, container) { - var self = this; - - var currentFetchPromise = this._currentFindFetchAllPromise; - if (isFetch && currentFetchPromise) { - return currentFetchPromise; - } else if (this._findAllRecordArray) { - if (isFetch) { - return new Ember.RSVP.Promise(function(resolve) { - resolve(self._findAllRecordArray); - }); - } else { - return this._findAllRecordArray; - } - } - - var records = this._findAllRecordArray = Ember.RecordArray.create({modelClass: this, container: container}); - - var promise = this._currentFindFetchAllPromise = this.adapter.findAll(this, records); - - promise.finally(function() { - self._currentFindFetchAllPromise = null; - }); - - // Remove the cached record array if the promise is rejected - if (promise.then) { - promise.then(null, function() { - self._findAllRecordArray = null; - return Ember.RSVP.reject.apply(null, arguments); - }); - } - - return isFetch ? promise : records; - }, - - findById: function(id) { - return this._findFetchById(id, false); - }, - - fetchById: function(id) { - return this._findFetchById(id, true); - }, - - _findFetchById: function(id, isFetch, container) { - var record = this.cachedRecordForId(id, container), - isLoaded = get(record, 'isLoaded'), - adapter = get(this, 'adapter'), - deferredOrPromise; - - if (isLoaded) { - if (isFetch) { - return new Ember.RSVP.Promise(function(resolve, reject) { - resolve(record); - }); - } else { - return record; - } - } - - deferredOrPromise = this._fetchById(record, id); - - return isFetch ? deferredOrPromise : record; - }, - - _currentBatchIds: null, - _currentBatchRecordArrays: null, - _currentBatchDeferreds: null, - - reload: function(id, container) { - var record = this.cachedRecordForId(id, container); - record.set('isLoaded', false); - return this._fetchById(record, id); - }, - - _fetchById: function(record, id) { - var adapter = get(this, 'adapter'), - deferred; - - if (adapter.findMany && !adapter.findMany.isUnimplemented) { - if (this._currentBatchIds) { - if (!contains(this._currentBatchIds, id)) { this._currentBatchIds.push(id); } - } else { - this._currentBatchIds = [id]; - this._currentBatchRecordArrays = []; - } - - deferred = Ember.Deferred.create(); - - //Attached the record to the deferred so we can resolve it later. - Ember.set(deferred, 'resolveWith', record); - - if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } - this._currentBatchDeferreds.push(deferred); - - Ember.run.scheduleOnce('data', this, this._executeBatch, record.container); - - return deferred; - } else { - return adapter.find(record, id); - } - }, - - _executeBatch: function(container) { - var batchIds = this._currentBatchIds, - batchRecordArrays = this._currentBatchRecordArrays, - batchDeferreds = this._currentBatchDeferreds, - self = this, - requestIds = [], - promise, - i; - - this._currentBatchIds = null; - this._currentBatchRecordArrays = null; - this._currentBatchDeferreds = null; - - for (i = 0; i < batchIds.length; i++) { - if (!this.cachedRecordForId(batchIds[i]).get('isLoaded')) { - requestIds.push(batchIds[i]); - } - } - - if (requestIds.length === 1) { - promise = get(this, 'adapter').find(this.cachedRecordForId(requestIds[0], container), requestIds[0]); - } else { - var recordArray = Ember.RecordArray.create({_ids: batchIds, container: container}); - if (requestIds.length === 0) { - promise = new Ember.RSVP.Promise(function(resolve, reject) { resolve(recordArray); }); - recordArray.notifyLoaded(); - } else { - promise = get(this, 'adapter').findMany(this, recordArray, requestIds); - } - } - - promise.then(function() { - for (var i = 0, l = batchRecordArrays.length; i < l; i++) { - batchRecordArrays[i].loadForFindMany(self); - } - - if (batchDeferreds) { - for (i = 0, l = batchDeferreds.length; i < l; i++) { - var resolveWith = Ember.get(batchDeferreds[i], 'resolveWith'); - batchDeferreds[i].resolve(resolveWith); - } - } - }).then(null, function(errorXHR) { - if (batchDeferreds) { - for (var i = 0, l = batchDeferreds.length; i < l; i++) { - batchDeferreds[i].reject(errorXHR); - } - } - }); - }, - - getCachedReferenceRecord: function(id, container){ - var ref = this._getReferenceById(id); - if(ref && ref.record) { - ref.record.container = container; - return ref.record; - } - return undefined; - }, - - cachedRecordForId: function(id, container) { - var record; - if (!this.transient) { - record = this.getCachedReferenceRecord(id, container); - } - - if (!record) { - var primaryKey = get(this, 'primaryKey'), - attrs = {isLoaded: false}; - - attrs[primaryKey] = id; - attrs.container = container; - record = this.create(attrs); - if (!this.transient) { - var sideloadedData = this.sideloadedData && this.sideloadedData[id]; - if (sideloadedData) { - record.load(id, sideloadedData); - } - } - } - - return record; - }, - - - addToRecordArrays: function(record) { - if (this._findAllRecordArray) { - this._findAllRecordArray.addObject(record); - } - if (this.recordArrays) { - this.recordArrays.forEach(function(recordArray) { - if (recordArray instanceof Ember.FilteredRecordArray) { - recordArray.registerObserversOnRecord(record); - recordArray.updateFilter(); - } else { - recordArray.addObject(record); - } - }); - } - }, - - unload: function (record) { - this.removeFromHasManyArrays(record); - this.removeFromRecordArrays(record); - var primaryKey = record.get(get(this, 'primaryKey')); - this.removeFromCache(primaryKey); - }, - - clearCache: function () { - this.sideloadedData = undefined; - this._referenceCache = undefined; - this._findAllRecordArray = undefined; - }, - - removeFromCache: function (key) { - if (this.sideloadedData && this.sideloadedData[key]) { - delete this.sideloadedData[key]; - } - if(this._referenceCache && this._referenceCache[key]) { - delete this._referenceCache[key]; - } - }, - - removeFromHasManyArrays: function(record) { - if (record._parentHasManyArrays) { - record._parentHasManyArrays.forEach(function(hasManyArray) { - hasManyArray.unloadObject(record); - }); - record._parentHasManyArrays = null; - } - }, - - removeFromRecordArrays: function(record) { - if (this._findAllRecordArray) { - this._findAllRecordArray.removeObject(record); - } - if (this.recordArrays) { - this.recordArrays.forEach(function(recordArray) { - recordArray.removeObject(record); - }); - } - }, - - // FIXME - findFromCacheOrLoad: function(data, container) { - var record; - if (!data[get(this, 'primaryKey')]) { - record = this.create({isLoaded: false, container: container}); - } else { - record = this.cachedRecordForId(data[get(this, 'primaryKey')], container); - } - // set(record, 'data', data); - record.load(data[get(this, 'primaryKey')], data); - return record; - }, - - registerRecordArray: function(recordArray) { - if (!this.recordArrays) { this.recordArrays = []; } - this.recordArrays.push(recordArray); - }, - - unregisterRecordArray: function(recordArray) { - if (!this.recordArrays) { return; } - Ember.A(this.recordArrays).removeObject(recordArray); - }, - - forEachCachedRecord: function(callback) { - if (!this._referenceCache) { return; } - var ids = Object.keys(this._referenceCache); - ids.map(function(id) { - return this._getReferenceById(id).record; - }, this).forEach(callback); - }, - - load: function(hashes) { - if (Ember.typeOf(hashes) !== 'array') { hashes = [hashes]; } - - if (!this.sideloadedData) { this.sideloadedData = {}; } - - for (var i = 0, l = hashes.length; i < l; i++) { - var hash = hashes[i], - primaryKey = hash[get(this, 'primaryKey')], - record = this.getCachedReferenceRecord(primaryKey); - - if (record) { - record.load(primaryKey, hash); - } else { - this.sideloadedData[primaryKey] = hash; - } - } - }, - - _getReferenceById: function(id) { - if (!this._referenceCache) { this._referenceCache = {}; } - return this._referenceCache[id]; - }, - - _getOrCreateReferenceForId: function(id) { - var reference = this._getReferenceById(id); - - if (!reference) { - reference = this._createReference(id); - } - - return reference; - }, - - _createReference: function(id) { - if (!this._referenceCache) { this._referenceCache = {}; } - - 0; - - var reference = { - id: id, - clientId: this._clientIdCounter++ - }; - - this._cacheReference(reference); - - return reference; - }, - - _cacheReference: function(reference) { - if (!this._referenceCache) { this._referenceCache = {}; } - - // if we're creating an item, this process will be done - // later, once the object has been persisted. - if (!Ember.isEmpty(reference.id)) { - this._referenceCache[reference.id] = reference; - } - } -}); - - -})(); - -(function() { - -var get = Ember.get; - -function getType(record) { - var type = this.type; - - if (typeof this.type === "string" && this.type) { - this.type = Ember.get(Ember.lookup, this.type); - - if (!this.type) { - var store = record.container.lookup('store:main'); - this.type = store.modelFor(type); - this.type.reopenClass({ adapter: store.adapterFor(type) }); - } - } - - return this.type; -} - -Ember.hasMany = function(type, options) { - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany', getType: getType}; - - return Ember.computed(function(propertyKey, newContentArray, existingArray) { - type = meta.getType(this); - 0; - - var key = options.key || propertyKey; - - if (arguments.length > 1) { - return existingArray.setObjects(newContentArray); - } else { - return this.getHasMany(key, type, meta, this.container); - } - }).property().meta(meta); -}; - -Ember.Model.reopen({ - getHasMany: function(key, type, meta, container) { - var embedded = meta.options.embedded, - collectionClass = embedded ? Ember.EmbeddedHasManyArray : Ember.HasManyArray; - - var collection = collectionClass.create({ - parent: this, - modelClass: type, - content: this._getHasManyContent(key, type, embedded), - embedded: embedded, - key: key, - relationshipKey: meta.relationshipKey, - container: container - }); - - this._registerHasManyArray(collection); - - return collection; - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set; - -function storeFor(record) { - if (record.container) { - return record.container.lookup('store:main'); - } - - return null; -} - -function getType(record) { - var type = this.type; - - if (typeof this.type === "string" && this.type) { - type = Ember.get(Ember.lookup, this.type); - - if (!type) { - var store = storeFor(record); - type = store.modelFor(this.type); - type.reopenClass({ adapter: store.adapterFor(this.type) }); - } - } - - return type; -} - -Ember.belongsTo = function(type, options) { - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo', getType: getType}; - - return Ember.computed(function(propertyKey, value, oldValue) { - type = meta.getType(this); - 0; - - var key = options.key || propertyKey; - - var dirtyAttributes = get(this, '_dirtyAttributes'), - createdDirtyAttributes = false, - self = this; - - var dirtyChanged = function(sender) { - if (sender.get('isDirty')) { - self._relationshipBecameDirty(key); - } else { - self._relationshipBecameClean(key); - } - }; - - if (!dirtyAttributes) { - dirtyAttributes = []; - createdDirtyAttributes = true; - } - - if (arguments.length > 1) { - - if (value) { - 0; - } - - if (oldValue !== value) { - dirtyAttributes.pushObject(propertyKey); - } else { - dirtyAttributes.removeObject(propertyKey); - } - - if (createdDirtyAttributes) { - set(this, '_dirtyAttributes', dirtyAttributes); - } - - if (meta.options.embedded) { - if (oldValue) { - oldValue.removeObserver('isDirty', dirtyChanged); - } - if (value) { - value.addObserver('isDirty', dirtyChanged); - } - } - - return value === undefined ? null : value; - } else { - var store = storeFor(this); - value = this.getBelongsTo(key, type, meta, store); - this._registerBelongsTo(meta); - if (value !== null && meta.options.embedded) { - value.get('isDirty'); // getter must be called before adding observer - value.addObserver('isDirty', dirtyChanged); - } - return value; - } - }).property('_data').meta(meta); -}; - -Ember.Model.reopen({ - getBelongsTo: function(key, type, meta, store) { - var idOrAttrs = get(this, '_data.' + key), - record; - - if (Ember.isNone(idOrAttrs)) { - return null; - } - - if (meta.options.embedded) { - var primaryKey = get(type, 'primaryKey'), - id = idOrAttrs[primaryKey]; - record = type.create({ isLoaded: false, id: id, container: this.container }); - record.load(id, idOrAttrs); - } else { - if (store) { - record = store._findSync(meta.type, idOrAttrs); - } else { - record = type.find(idOrAttrs); - } - } - - return record; - } -}); - - -})(); - -(function() { - -var get = Ember.get, - set = Ember.set, - meta = Ember.meta; - -Ember.Model.dataTypes = {}; - -Ember.Model.dataTypes[Date] = { - deserialize: function(string) { - if (!string) { return null; } - return new Date(string); - }, - serialize: function (date) { - if (!date) { return null; } - return date.toISOString(); - }, - isEqual: function(obj1, obj2) { - if (obj1 instanceof Date) { obj1 = this.serialize(obj1); } - if (obj2 instanceof Date) { obj2 = this.serialize(obj2); } - return obj1 === obj2; - } -}; - -Ember.Model.dataTypes[Number] = { - deserialize: function(string) { - if (!string && string !== 0) { return null; } - return Number(string); - }, - serialize: function (number) { - if (!number && number !== 0) { return null; } - return Number(number); - } -}; - -function deserialize(value, type) { - if (type && type.deserialize) { - return type.deserialize(value); - } else if (type && Ember.Model.dataTypes[type]) { - return Ember.Model.dataTypes[type].deserialize(value); - } else { - return value; - } -} - -function serialize(value, type) { - if (type && type.serialize) { - return type.serialize(value); - } else if (type && Ember.Model.dataTypes[type]) { - return Ember.Model.dataTypes[type].serialize(value); - } else { - return value; - } -} - -Ember.attr = function(type, options) { - return Ember.computed(function(key, value) { - var data = get(this, '_data'), - dataKey = this.dataKey(key), - dataValue = data && get(data, dataKey), - beingCreated = meta(this).proto === this, - dirtyAttributes = get(this, '_dirtyAttributes'), - createdDirtyAttributes = false; - - if (!dirtyAttributes) { - dirtyAttributes = []; - createdDirtyAttributes = true; - } - - if (arguments.length === 2) { - if (beingCreated) { - if (!data) { - data = {}; - set(this, '_data', data); - } - dataValue = data[dataKey] = value; - } - - if (dataValue !== serialize(value, type)) { - dirtyAttributes.pushObject(key); - } else { - dirtyAttributes.removeObject(key); - } - - if (createdDirtyAttributes) { - set(this, '_dirtyAttributes', dirtyAttributes); - } - - return value; - } - - if (dataValue==null && options && options.defaultValue!=null) { - return Ember.copy(options.defaultValue); - } - - return this.getAttr(key, deserialize(dataValue, type)); - }).property('_data').meta({isAttribute: true, type: type, options: options}); -}; - - -})(); - -(function() { - -var get = Ember.get; - -Ember.RESTAdapter = Ember.Adapter.extend({ - find: function(record, id) { - var url = this.buildURL(record.constructor, id), - self = this; - - return this.ajax(url).then(function(data) { - self.didFind(record, id, data); - return record; - }); - }, - - didFind: function(record, id, data) { - var rootKey = get(record.constructor, 'rootKey'), - dataToLoad = rootKey ? get(data, rootKey) : data; - - record.load(id, dataToLoad); - }, - - findAll: function(klass, records) { - var url = this.buildURL(klass), - self = this; - - return this.ajax(url).then(function(data) { - self.didFindAll(klass, records, data); - return records; - }); - }, - - didFindAll: function(klass, records, data) { - var collectionKey = get(klass, 'collectionKey'), - dataToLoad = collectionKey ? get(data, collectionKey) : data; - - records.load(klass, dataToLoad); - }, - - findQuery: function(klass, records, params) { - var url = this.buildURL(klass), - self = this; - - return this.ajax(url, params).then(function(data) { - self.didFindQuery(klass, records, params, data); - return records; - }); - }, - - didFindQuery: function(klass, records, params, data) { - var collectionKey = get(klass, 'collectionKey'), - dataToLoad = collectionKey ? get(data, collectionKey) : data; - - records.load(klass, dataToLoad); - }, - - createRecord: function(record) { - var url = this.buildURL(record.constructor), - self = this; - - return this.ajax(url, record.toJSON(), "POST").then(function(data) { - self.didCreateRecord(record, data); - return record; - }); - }, - - didCreateRecord: function(record, data) { - this._loadRecordFromData(record, data); - record.didCreateRecord(); - }, - - saveRecord: function(record) { - var primaryKey = get(record.constructor, 'primaryKey'), - url = this.buildURL(record.constructor, get(record, primaryKey)), - self = this; - - return this.ajax(url, record.toJSON(), "PUT").then(function(data) { // TODO: Some APIs may or may not return data - self.didSaveRecord(record, data); - return record; - }); - }, - - didSaveRecord: function(record, data) { - this._loadRecordFromData(record, data); - record.didSaveRecord(); - }, - - deleteRecord: function(record) { - var primaryKey = get(record.constructor, 'primaryKey'), - url = this.buildURL(record.constructor, get(record, primaryKey)), - self = this; - - return this.ajax(url, record.toJSON(), "DELETE").then(function(data) { // TODO: Some APIs may or may not return data - self.didDeleteRecord(record, data); - }); - }, - - didDeleteRecord: function(record, data) { - record.didDeleteRecord(); - }, - - ajax: function(url, params, method, settings) { - return this._ajax(url, params, (method || "GET"), settings); - }, - - buildURL: function(klass, id) { - var urlRoot = get(klass, 'url'); - var urlSuffix = get(klass, 'urlSuffix') || ''; - if (!urlRoot) { throw new Error('Ember.RESTAdapter requires a `url` property to be specified'); } - - if (!Ember.isEmpty(id)) { - return urlRoot + "/" + id + urlSuffix; - } else { - return urlRoot + urlSuffix; - } - }, - - ajaxSettings: function(url, method) { - return { - url: url, - type: method, - dataType: "json" - }; - }, - - _ajax: function(url, params, method, settings) { - if (!settings) { - settings = this.ajaxSettings(url, method); - } - - return new Ember.RSVP.Promise(function(resolve, reject) { - if (params) { - if (method === "GET") { - settings.data = params; - } else { - settings.contentType = "application/json; charset=utf-8"; - settings.data = JSON.stringify(params); - } - } - - settings.success = function(json) { - Ember.run(null, resolve, json); - }; - - settings.error = function(jqXHR, textStatus, errorThrown) { - // https://github.com/ebryn/ember-model/issues/202 - if (jqXHR && typeof jqXHR === 'object') { - jqXHR.then = null; - } - - Ember.run(null, reject, jqXHR); - }; - - - Ember.$.ajax(settings); - }); - }, - - _loadRecordFromData: function(record, data) { - var rootKey = get(record.constructor, 'rootKey'), - primaryKey = get(record.constructor, 'primaryKey'); - // handle HEAD response where no data is provided by server - if (data) { - data = rootKey ? get(data, rootKey) : data; - if (!Ember.isEmpty(data)) { - record.load(data[primaryKey], data); - } - } - } -}); - - -})(); - -(function() { - -var get = Ember.get; - -Ember.LoadPromise = Ember.Object.extend(Ember.DeferredMixin, { - init: function() { - this._super.apply(this, arguments); - - var target = get(this, 'target'); - - if (get(target, 'isLoaded') && !get(target, 'isNew')) { - this.resolve(target); - } else { - target.one('didLoad', this, function() { - this.resolve(target); - }); - } - } -}); - -Ember.loadPromise = function(target) { - if (Ember.isNone(target)) { - return null; - } else if (target.then) { - return target; - } else { - return Ember.LoadPromise.create({target: target}); - } -}; - - -})(); - -(function() { - -// This is a debug adapter for the Ember Extension, don't let the fact this is called an "adapter" confuse you. -// Most copied from: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/debug/debug_adapter.js - -if (!Ember.DataAdapter) { return; } - -var get = Ember.get, capitalize = Ember.String.capitalize, underscore = Ember.String.underscore; - -var DebugAdapter = Ember.DataAdapter.extend({ - getFilters: function() { - return [ - { name: 'isNew', desc: 'New' }, - { name: 'isModified', desc: 'Modified' }, - { name: 'isClean', desc: 'Clean' } - ]; - }, - - detect: function(klass) { - return klass !== Ember.Model && Ember.Model.detect(klass); - }, - - columnsForType: function(type) { - var columns = [], count = 0, self = this; - type.getAttributes().forEach(function(name, meta) { - if (count++ > self.attributeLimit) { return false; } - var desc = capitalize(underscore(name).replace('_', ' ')); - columns.push({ name: name, desc: desc }); - }); - return columns; - }, - - getRecords: function(type) { - var records = []; - type.forEachCachedRecord(function(record) { records.push(record); }); - return records; - }, - - getRecordColumnValues: function(record) { - var self = this, count = 0, - columnValues = { id: get(record, 'id') }; - - record.constructor.getAttributes().forEach(function(key) { - if (count++ > self.attributeLimit) { - return false; - } - var value = get(record, key); - columnValues[key] = value; - }); - return columnValues; - }, - - getRecordKeywords: function(record) { - var keywords = [], keys = Ember.A(['id']); - record.constructor.getAttributes().forEach(function(key) { - keys.push(key); - }); - keys.forEach(function(key) { - keywords.push(get(record, key)); - }); - return keywords; - }, - - getRecordFilterValues: function(record) { - return { - isNew: record.get('isNew'), - isModified: record.get('isDirty') && !record.get('isNew'), - isClean: !record.get('isDirty') - }; - }, - - getRecordColor: function(record) { - var color = 'black'; - if (record.get('isNew')) { - color = 'green'; - } else if (record.get('isDirty')) { - color = 'blue'; - } - return color; - }, - - observeRecord: function(record, recordUpdated) { - var releaseMethods = Ember.A(), self = this, - keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); - - record.constructor.getAttributes().forEach(function(key) { - keysToObserve.push(key); - }); - - keysToObserve.forEach(function(key) { - var handler = function() { - recordUpdated(self.wrapRecord(record)); - }; - Ember.addObserver(record, key, handler); - releaseMethods.push(function() { - Ember.removeObserver(record, key, handler); - }); - }); - - var release = function() { - releaseMethods.forEach(function(fn) { fn(); } ); - }; - - return release; - } -}); - -Ember.onLoad('Ember.Application', function(Application) { - Application.initializer({ - name: "data-adapter", - - initialize: function(container, application) { - application.register('data-adapter:main', DebugAdapter); - } - }); -}); - - -})(); - -(function() { - -function NIL() {} - -Ember.Model.Store = Ember.Object.extend({ - container: null, - - modelFor: function(type) { - return this.container.lookupFactory('model:'+type); - }, - - adapterFor: function(type) { - var adapter = this.modelFor(type).adapter, - container = this.container; - - if (adapter && adapter !== Ember.Model.adapter) { - return adapter; - } else { - adapter = container.lookupFactory('adapter:'+ type) || - container.lookupFactory('adapter:application') || - container.lookupFactory('adapter:REST'); - - return adapter ? adapter.create() : adapter; - } - }, - - createRecord: function(type) { - var klass = this.modelFor(type); - klass.reopenClass({adapter: this.adapterFor(type)}); - return klass.create({container: this.container}); - }, - - find: function(type, id) { - if (arguments.length === 1) { id = NIL; } - return this._find(type, id, true); - }, - - _find: function(type, id, async) { - var klass = this.modelFor(type); - - // if (!klass.adapter) { - klass.reopenClass({adapter: this.adapterFor(type)}); - // } - - if (id === NIL) { - return klass._findFetchAll(async, this.container); - } else if (Ember.isArray(id)) { - return klass._findFetchMany(id, async, this.container); - } else if (typeof id === 'object') { - return klass._findFetchQuery(id, async, this.container); - } else { - return klass._findFetchById(id, async, this.container); - } - }, - - _findSync: function(type, id) { - return this._find(type, id, false); - } -}); - -Ember.onLoad('Ember.Application', function(Application) { - Application.initializer({ - name: "store", - - initialize: function(container, application) { - application.register('store:main', container.lookupFactory('store:application') || Ember.Model.Store); - - application.inject('route', 'store', 'store:main'); - application.inject('controller', 'store', 'store:main'); - } - }); -}); - - -})(); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-validations.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-validations.js deleted file mode 100644 index b8bc45b1f..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/ember-validations.js +++ /dev/null @@ -1,779 +0,0 @@ -// ========================================================================== -// Project: Ember Validations -// Copyright: Copyright 2013 DockYard, LLC. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== - - - // Version: 1.0.0 - -(function() { -Ember.Validations = Ember.Namespace.create({ - VERSION: '1.0.0' -}); - -})(); - - - -(function() { -Ember.Validations.messages = { - render: function(attribute, context) { - if (Ember.I18n) { - return Ember.I18n.t('errors.' + attribute, context); - } else { - var regex = new RegExp("{{(.*?)}}"), - attributeName = ""; - if (regex.test(this.defaults[attribute])) { - attributeName = regex.exec(this.defaults[attribute])[1]; - } - return this.defaults[attribute].replace(regex, context[attributeName]); - } - }, - defaults: { - inclusion: "is not included in the list", - exclusion: "is reserved", - invalid: "is invalid", - confirmation: "doesn't match {{attribute}}", - accepted: "must be accepted", - empty: "can't be empty", - blank: "can't be blank", - present: "must be blank", - tooLong: "is too long (maximum is {{count}} characters)", - tooShort: "is too short (minimum is {{count}} characters)", - wrongLength: "is the wrong length (should be {{count}} characters)", - notANumber: "is not a number", - notAnInteger: "must be an integer", - greaterThan: "must be greater than {{count}}", - greaterThanOrEqualTo: "must be greater than or equal to {{count}}", - equalTo: "must be equal to {{count}}", - lessThan: "must be less than {{count}}", - lessThanOrEqualTo: "must be less than or equal to {{count}}", - otherThan: "must be other than {{count}}", - odd: "must be odd", - even: "must be even", - url: "is not a valid URL" - } -}; - -})(); - - - -(function() { -Ember.Validations.Errors = Ember.Object.extend({ - unknownProperty: function(property) { - this.set(property, Ember.makeArray()); - return this.get(property); - } -}); - -})(); - - - -(function() { -var setValidityMixin = Ember.Mixin.create({ - isValid: function() { - return this.get('validators').compact().filterBy('isValid', false).get('length') === 0; - }.property('validators.@each.isValid'), - isInvalid: Ember.computed.not('isValid') -}); - -var pushValidatableObject = function(model, property) { - var content = model.get(property); - - model.removeObserver(property, pushValidatableObject); - if (Ember.isArray(content)) { - model.validators.pushObject(ArrayValidatorProxy.create({model: model, property: property, contentBinding: 'model.' + property})); - } else { - model.validators.pushObject(content); - } -}; - -var findValidator = function(validator) { - var klass = validator.classify(); - return Ember.Validations.validators.local[klass] || Ember.Validations.validators.remote[klass]; -}; - -var ArrayValidatorProxy = Ember.ArrayProxy.extend(setValidityMixin, { - validate: function() { - return this._validate(); - }, - _validate: function() { - var promises = this.get('content').invoke('_validate').without(undefined); - return Ember.RSVP.all(promises); - }.on('init'), - validators: Ember.computed.alias('content') -}); - -Ember.Validations.Mixin = Ember.Mixin.create(setValidityMixin, { - init: function() { - this._super(); - this.errors = Ember.Validations.Errors.create(); - this._dependentValidationKeys = {}; - this.validators = Ember.makeArray(); - if (this.get('validations') === undefined) { - this.validations = {}; - } - this.buildValidators(); - this.validators.forEach(function(validator) { - validator.addObserver('errors.[]', this, function(sender, key, value, context, rev) { - var errors = Ember.makeArray(); - this.validators.forEach(function(validator) { - if (validator.property === sender.property) { - errors = errors.concat(validator.errors); - } - }, this); - this.set('errors.' + sender.property, errors); - }); - }, this); - }, - buildValidators: function() { - var property, validator; - - for (property in this.validations) { - if (this.validations[property].constructor === Object) { - this.buildRuleValidator(property); - } else { - this.buildObjectValidator(property); - } - } - }, - buildRuleValidator: function(property) { - var validator; - for (validator in this.validations[property]) { - if (this.validations[property].hasOwnProperty(validator)) { - this.validators.pushObject(findValidator(validator).create({model: this, property: property, options: this.validations[property][validator]})); - } - } - }, - buildObjectValidator: function(property) { - if (Ember.isNone(this.get(property))) { - this.addObserver(property, this, pushValidatableObject); - } else { - pushValidatableObject(this, property); - } - }, - validate: function() { - var self = this; - return this._validate().then(function(vals) { - var errors = self.get('errors'); - if (vals.contains(false)) { - return Ember.RSVP.reject(errors); - } - return errors; - }); - }, - _validate: function() { - var promises = this.validators.invoke('_validate').without(undefined); - return Ember.RSVP.all(promises); - }.on('init') -}); - -})(); - - - -(function() { -Ember.Validations.patterns = Ember.Namespace.create({ - numericality: /^(-|\+)?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d*)?$/, - blank: /^\s*$/ -}); - -})(); - - - -(function() { -Ember.Validations.validators = Ember.Namespace.create(); -Ember.Validations.validators.local = Ember.Namespace.create(); -Ember.Validations.validators.remote = Ember.Namespace.create(); - -})(); - - - -(function() { -Ember.Validations.validators.Base = Ember.Object.extend({ - init: function() { - this.set('errors', Ember.makeArray()); - this._dependentValidationKeys = Ember.makeArray(); - this.conditionals = { - 'if': this.get('options.if'), - unless: this.get('options.unless') - }; - this.model.addObserver(this.property, this, this._validate); - }, - addObserversForDependentValidationKeys: function() { - this._dependentValidationKeys.forEach(function(key) { - this.model.addObserver(key, this, this._validate); - }, this); - }.on('init'), - pushDependentValidationKeyToModel: function() { - var model = this.get('model'); - if (model._dependentValidationKeys[this.property] === undefined) { - model._dependentValidationKeys[this.property] = Ember.makeArray(); - } - model._dependentValidationKeys[this.property].addObjects(this._dependentValidationKeys); - }.on('init'), - call: function () { - throw 'Not implemented!'; - }, - unknownProperty: function(key) { - var model = this.get('model'); - if (model) { - return model.get(key); - } - }, - isValid: Ember.computed.empty('errors.[]'), - validate: function() { - var self = this; - return this._validate().then(function(success) { - // Convert validation failures to rejects. - var errors = self.get('model.errors'); - if (success) { - return errors; - } else { - return Ember.RSVP.reject(errors); - } - }); - }, - _validate: function() { - this.errors.clear(); - if (this.canValidate()) { - this.call(); - } - if (this.get('isValid')) { - return Ember.RSVP.resolve(true); - } else { - return Ember.RSVP.resolve(false); - } - }.on('init'), - canValidate: function() { - if (typeof(this.conditionals) === 'object') { - if (this.conditionals['if']) { - if (typeof(this.conditionals['if']) === 'function') { - return this.conditionals['if'](this.model, this.property); - } else if (typeof(this.conditionals['if']) === 'string') { - if (typeof(this.model[this.conditionals['if']]) === 'function') { - return this.model[this.conditionals['if']](); - } else { - return this.model.get(this.conditionals['if']); - } - } - } else if (this.conditionals.unless) { - if (typeof(this.conditionals.unless) === 'function') { - return !this.conditionals.unless(this.model, this.property); - } else if (typeof(this.conditionals.unless) === 'string') { - if (typeof(this.model[this.conditionals.unless]) === 'function') { - return !this.model[this.conditionals.unless](); - } else { - return !this.model.get(this.conditionals.unless); - } - } - } else { - return true; - } - } else { - return true; - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Absence = Ember.Validations.validators.Base.extend({ - init: function() { - this._super(); - /*jshint expr:true*/ - if (this.options === true) { - this.set('options', {}); - } - - if (this.options.message === undefined) { - this.set('options.message', Ember.Validations.messages.render('present', this.options)); - } - }, - call: function() { - if (!Ember.isEmpty(this.model.get(this.property))) { - this.errors.pushObject(this.options.message); - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Acceptance = Ember.Validations.validators.Base.extend({ - init: function() { - this._super(); - /*jshint expr:true*/ - if (this.options === true) { - this.set('options', {}); - } - - if (this.options.message === undefined) { - this.set('options.message', Ember.Validations.messages.render('accepted', this.options)); - } - }, - call: function() { - if (this.options.accept) { - if (this.model.get(this.property) !== this.options.accept) { - this.errors.pushObject(this.options.message); - } - } else if (this.model.get(this.property) !== '1' && this.model.get(this.property) !== 1 && this.model.get(this.property) !== true) { - this.errors.pushObject(this.options.message); - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Confirmation = Ember.Validations.validators.Base.extend({ - init: function() { - this.originalProperty = this.property; - this.property = this.property + 'Confirmation'; - this._super(); - this._dependentValidationKeys.pushObject(this.originalProperty); - /*jshint expr:true*/ - if (this.options === true) { - this.set('options', { attribute: this.originalProperty }); - this.set('options', { message: Ember.Validations.messages.render('confirmation', this.options) }); - } - }, - call: function() { - if (this.model.get(this.originalProperty) !== this.model.get(this.property)) { - this.errors.pushObject(this.options.message); - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Exclusion = Ember.Validations.validators.Base.extend({ - init: function() { - this._super(); - if (this.options.constructor === Array) { - this.set('options', { 'in': this.options }); - } - - if (this.options.message === undefined) { - this.set('options.message', Ember.Validations.messages.render('exclusion', this.options)); - } - }, - call: function() { - /*jshint expr:true*/ - var message, lower, upper; - - if (Ember.isEmpty(this.model.get(this.property))) { - if (this.options.allowBlank === undefined) { - this.errors.pushObject(this.options.message); - } - } else if (this.options['in']) { - if (Ember.$.inArray(this.model.get(this.property), this.options['in']) !== -1) { - this.errors.pushObject(this.options.message); - } - } else if (this.options.range) { - lower = this.options.range[0]; - upper = this.options.range[1]; - - if (this.model.get(this.property) >= lower && this.model.get(this.property) <= upper) { - this.errors.pushObject(this.options.message); - } - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Format = Ember.Validations.validators.Base.extend({ - init: function() { - this._super(); - if (this.options.constructor === RegExp) { - this.set('options', { 'with': this.options }); - } - - if (this.options.message === undefined) { - this.set('options.message', Ember.Validations.messages.render('invalid', this.options)); - } - }, - call: function() { - if (Ember.isEmpty(this.model.get(this.property))) { - if (this.options.allowBlank === undefined) { - this.errors.pushObject(this.options.message); - } - } else if (this.options['with'] && !this.options['with'].test(this.model.get(this.property))) { - this.errors.pushObject(this.options.message); - } else if (this.options.without && this.options.without.test(this.model.get(this.property))) { - this.errors.pushObject(this.options.message); - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Inclusion = Ember.Validations.validators.Base.extend({ - init: function() { - this._super(); - if (this.options.constructor === Array) { - this.set('options', { 'in': this.options }); - } - - if (this.options.message === undefined) { - this.set('options.message', Ember.Validations.messages.render('inclusion', this.options)); - } - }, - call: function() { - var message, lower, upper; - if (Ember.isEmpty(this.model.get(this.property))) { - if (this.options.allowBlank === undefined) { - this.errors.pushObject(this.options.message); - } - } else if (this.options['in']) { - if (Ember.$.inArray(this.model.get(this.property), this.options['in']) === -1) { - this.errors.pushObject(this.options.message); - } - } else if (this.options.range) { - lower = this.options.range[0]; - upper = this.options.range[1]; - - if (this.model.get(this.property) < lower || this.model.get(this.property) > upper) { - this.errors.pushObject(this.options.message); - } - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Length = Ember.Validations.validators.Base.extend({ - init: function() { - var index, key; - this._super(); - /*jshint expr:true*/ - if (typeof(this.options) === 'number') { - this.set('options', { 'is': this.options }); - } - - if (this.options.messages === undefined) { - this.set('options.messages', {}); - } - - for (index = 0; index < this.messageKeys().length; index++) { - key = this.messageKeys()[index]; - if (this.options[key] !== undefined && this.options[key].constructor === String) { - this.model.addObserver(this.options[key], this, this._validate); - } - } - - this.options.tokenizer = this.options.tokenizer || function(value) { return value.split(''); }; - // if (typeof(this.options.tokenizer) === 'function') { - // debugger; - // // this.tokenizedLength = new Function('value', 'return ' - // } else { - // this.tokenizedLength = new Function('value', 'return (value || "").' + (this.options.tokenizer || 'split("")') + '.length'); - // } - }, - CHECKS: { - 'is' : '==', - 'minimum' : '>=', - 'maximum' : '<=' - }, - MESSAGES: { - 'is' : 'wrongLength', - 'minimum' : 'tooShort', - 'maximum' : 'tooLong' - }, - getValue: function(key) { - if (this.options[key].constructor === String) { - return this.model.get(this.options[key]) || 0; - } else { - return this.options[key]; - } - }, - messageKeys: function() { - return Ember.keys(this.MESSAGES); - }, - checkKeys: function() { - return Ember.keys(this.CHECKS); - }, - renderMessageFor: function(key) { - var options = {count: this.getValue(key)}, _key; - for (_key in this.options) { - options[_key] = this.options[_key]; - } - - return this.options.messages[this.MESSAGES[key]] || Ember.Validations.messages.render(this.MESSAGES[key], options); - }, - renderBlankMessage: function() { - if (this.options.is) { - return this.renderMessageFor('is'); - } else if (this.options.minimum) { - return this.renderMessageFor('minimum'); - } - }, - call: function() { - var check, fn, message, operator, key; - - if (Ember.isEmpty(this.model.get(this.property))) { - if (this.options.allowBlank === undefined && (this.options.is || this.options.minimum)) { - this.errors.pushObject(this.renderBlankMessage()); - } - } else { - for (key in this.CHECKS) { - operator = this.CHECKS[key]; - if (!this.options[key]) { - continue; - } - - fn = new Function('return ' + this.options.tokenizer(this.model.get(this.property)).length + ' ' + operator + ' ' + this.getValue(key)); - if (!fn()) { - this.errors.pushObject(this.renderMessageFor(key)); - } - } - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Numericality = Ember.Validations.validators.Base.extend({ - init: function() { - /*jshint expr:true*/ - var index, keys, key; - this._super(); - - if (this.options === true) { - this.options = {}; - } else if (this.options.constructor === String) { - key = this.options; - this.options = {}; - this.options[key] = true; - } - - if (this.options.messages === undefined || this.options.messages.numericality === undefined) { - this.options.messages = this.options.messages || {}; - this.options.messages = { numericality: Ember.Validations.messages.render('notANumber', this.options) }; - } - - if (this.options.onlyInteger !== undefined && this.options.messages.onlyInteger === undefined) { - this.options.messages.onlyInteger = Ember.Validations.messages.render('notAnInteger', this.options); - } - - keys = Ember.keys(this.CHECKS).concat(['odd', 'even']); - for(index = 0; index < keys.length; index++) { - key = keys[index]; - - var prop = this.options[key]; - if (key in this.options && isNaN(prop)) { - this.model.addObserver(prop, this, this._validate); - } - - if (prop !== undefined && this.options.messages[key] === undefined) { - if (Ember.$.inArray(key, Ember.keys(this.CHECKS)) !== -1) { - this.options.count = prop; - } - this.options.messages[key] = Ember.Validations.messages.render(key, this.options); - if (this.options.count !== undefined) { - delete this.options.count; - } - } - } - }, - CHECKS: { - equalTo :'===', - greaterThan : '>', - greaterThanOrEqualTo : '>=', - lessThan : '<', - lessThanOrEqualTo : '<=' - }, - call: function() { - var check, checkValue, fn, form, operator, val; - - if (Ember.isEmpty(this.model.get(this.property))) { - if (this.options.allowBlank === undefined) { - this.errors.pushObject(this.options.messages.numericality); - } - } else if (!Ember.Validations.patterns.numericality.test(this.model.get(this.property))) { - this.errors.pushObject(this.options.messages.numericality); - } else if (this.options.onlyInteger === true && !(/^[+\-]?\d+$/.test(this.model.get(this.property)))) { - this.errors.pushObject(this.options.messages.onlyInteger); - } else if (this.options.odd && parseInt(this.model.get(this.property), 10) % 2 === 0) { - this.errors.pushObject(this.options.messages.odd); - } else if (this.options.even && parseInt(this.model.get(this.property), 10) % 2 !== 0) { - this.errors.pushObject(this.options.messages.even); - } else { - for (check in this.CHECKS) { - operator = this.CHECKS[check]; - - if (this.options[check] === undefined) { - continue; - } - - if (!isNaN(parseFloat(this.options[check])) && isFinite(this.options[check])) { - checkValue = this.options[check]; - } else if (this.model.get(this.options[check]) !== undefined) { - checkValue = this.model.get(this.options[check]); - } - - fn = new Function('return ' + this.model.get(this.property) + ' ' + operator + ' ' + checkValue); - - if (!fn()) { - this.errors.pushObject(this.options.messages[check]); - } - } - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Presence = Ember.Validations.validators.Base.extend({ - init: function() { - this._super(); - /*jshint expr:true*/ - if (this.options === true) { - this.options = {}; - } - - if (this.options.message === undefined) { - this.options.message = Ember.Validations.messages.render('blank', this.options); - } - }, - call: function() { - if (Ember.isEmpty(this.model.get(this.property))) { - this.errors.pushObject(this.options.message); - } - } -}); - -})(); - - - -(function() { -Ember.Validations.validators.local.Url = Ember.Validations.validators.Base.extend({ - regexp: null, - regexp_ip: null, - - init: function() { - this._super(); - - if (this.get('options.message') === undefined) { - this.set('options.message', Ember.Validations.messages.render('url', this.options)); - } - - if (this.get('options.protocols') === undefined) { - this.set('options.protocols', ['http', 'https']); - } - - // Regular Expression Parts - var dec_octet = '(25[0-5]|2[0-4][0-9]|[0-1][0-9][0-9]|[1-9][0-9]|[0-9])'; // 0-255 - var ipaddress = '(' + dec_octet + '(\\.' + dec_octet + '){3})'; - var hostname = '([a-zA-Z0-9\\-]+\\.)+([a-zA-Z]{2,})'; - var encoded = '%[0-9a-fA-F]{2}'; - var characters = 'a-zA-Z0-9$\\-_.+!*\'(),;:@&='; - var segment = '([' + characters + ']|' + encoded + ')*'; - - // Build Regular Expression - var regex_str = '^'; - - if (this.get('options.domainOnly') === true) { - regex_str += hostname; - } else { - regex_str += '(' + this.get('options.protocols').join('|') + '):\\/\\/'; // Protocol - - // Username and password - if (this.get('options.allowUserPass') === true) { - regex_str += '(([a-zA-Z0-9$\\-_.+!*\'(),;:&=]|' + encoded + ')+@)?'; // Username & passwords - } - - // IP Addresses? - if (this.get('options.allowIp') === true) { - regex_str += '(' + hostname + '|' + ipaddress + ')'; // Hostname OR IP - } else { - regex_str += '(' + hostname + ')'; // Hostname only - } - - // Ports - if (this.get('options.allowPort') === true) { - regex_str += '(:[0-9]+)?'; // Port - } - - regex_str += '(\\/'; - regex_str += '(' + segment + '(\\/' + segment + ')*)?'; // Path - regex_str += '(\\?' + '([' + characters + '/?]|' + encoded + ')*)?'; // Query - regex_str += '(\\#' + '([' + characters + '/?]|' + encoded + ')*)?'; // Anchor - regex_str += ')?'; - } - - regex_str += '$'; - - // RegExp - this.regexp = new RegExp(regex_str); - this.regexp_ip = new RegExp(ipaddress); - }, - call: function() { - var url = this.model.get(this.property); - - if (Ember.isEmpty(url)) { - if (this.get('options.allowBlank') !== true) { - this.errors.pushObject(this.get('options.message')); - } - } else { - if (this.get('options.allowIp') !== true) { - if (this.regexp_ip.test(url)) { - this.errors.pushObject(this.get('options.message')); - return; - } - } - - if (!this.regexp.test(url)) { - this.errors.pushObject(this.get('options.message')); - } - } - } -}); - - -})(); - - - -(function() { - -})(); - - - -(function() { - -})(); - diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/da.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/da.js deleted file mode 100644 index 156301028..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/da.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * Oversat af Jna Borup Coyle, github@coyle.dk - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Tilføj regel", - "add_group": "Tilføj gruppe", - "delete_rule": "Slet regel", - "delete_group": "Slet gruppe", - - "condition_and": "OG", - "condition_or": "ELLER", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "lig med", - "not_equal": "ikke lige med", - "in": "i", - "not_in": "ikke i", - "less": "mindre", - "less_or_equal": "mindre eller lig med", - "greater": "større", - "greater_or_equal": "større eller lig med", - "begins_with": "begynder med", - "not_begins_with": "begynder ikke med", - "contains": "indeholder", - "not_contains": "indeholder ikke", - "ends_with": "slutter med", - "not_ends_with": "slutter ikke med", - "is_empty": "er tom", - "is_not_empty": "er ikke tom", - "is_null": "er null", - "is_not_null": "er ikke null" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/de.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/de.js deleted file mode 100644 index 988b7826d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/de.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * German translation - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "neue Regel", - "add_group": "neue Gruppe", - "delete_rule": "löschen", - "delete_group": "löschen", - - "condition_and": "UND", - "condition_or": "ODER", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "gleich", - "not_equal": "ungleich", - "in": "in", - "not_in": "nicht in", - "less": "kleiner", - "less_or_equal": "kleiner gleich", - "greater": "größer", - "greater_or_equal": "größer gleich", - "begins_with": "beginnt mit", - "not_begins_with": "beginnt nicht mit", - "contains": "enthält", - "not_contains": "enthält nicht", - "ends_with": "endet mit", - "not_ends_with": "endet nicht mit", - "is_empty": "ist leer", - "is_not_empty": "ist nicht leer", - "is_null": "ist null", - "is_not_null": "ist nicht null" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/en.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/en.js deleted file mode 100644 index 513e915a3..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/en.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * Reference language file - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Add rule", - "add_group": "Add group", - "delete_rule": "Delete", - "delete_group": "Delete", - - "condition_and": "AND", - "condition_or": "OR", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "equal", - "not_equal": "not equal", - "in": "in", - "not_in": "not in", - "less": "less", - "less_or_equal": "less or equal", - "greater": "greater", - "greater_or_equal": "greater or equal", - "between": "between", - "begins_with": "begins with", - "not_begins_with": "doesn't begin with", - "contains": "contains", - "not_contains": "doesn't contain", - "ends_with": "ends with", - "not_ends_with": "doesn't end with", - "is_empty": "is empty", - "is_not_empty": "is not empty", - "is_null": "is null", - "is_not_null": "is not null" - }, - - "errors": { - "no_filter": "No filter selected", - "empty_group": "The group is empty", - "radio_empty": "No value selected", - "checkbox_empty": "No value selected", - "select_empty": "No value selected", - "string_empty": "Empty value", - "string_exceed_min_length": "Must contain at least {0} characters", - "string_exceed_max_length": "Must not contain more than {0} characters", - "string_invalid_format": "Invalid format ({0})", - "number_nan": "Not a number", - "number_not_integer": "Not an integer", - "number_not_double": "Not a real number", - "number_exceed_min": "Must be greater than {0}", - "number_exceed_max": "Must be lower than {0}", - "number_wrong_step": "Must be a multiple of {0}", - "datetime_invalid": "Invalid date format ({0})", - "datetime_exceed_min": "Must be after {0}", - "datetime_exceed_max": "Must be before {0}" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/es.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/es.js deleted file mode 100644 index bc3ea3a7b..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/es.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * Spanish translation by "pyarza" - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Añadir regla", - "add_group": "Añadir grupo", - "delete_rule": "Borrar", - "delete_group": "Borrar", - - "condition_and": "Y", - "condition_or": "O", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "igual", - "not_equal": "distinto", - "in": "en", - "not_in": "no en", - "less": "menor", - "less_or_equal": "menor o igual", - "greater": "mayor", - "greater_or_equal": "mayor o igual", - "between": "entre", - "begins_with": "empieza por", - "not_begins_with": "no empieza por", - "contains": "contiene", - "not_contains": "no contiene", - "ends_with": "acaba con", - "not_ends_with": "no acaba con", - "is_empty": "esta vacio", - "is_not_empty": "no esta vacio", - "is_null": "es nulo", - "is_not_null": "no es nulo" - }, - - "errors": { - "no_filter": "No se ha seleccionado ningun filtro", - "empty_group": "El grupo esta vacio", - "radio_empty": "Ningun valor seleccionado", - "checkbox_empty": "Ningun valor seleccionado", - "select_empty": "Ningun valor seleccionado", - "string_empty": "Cadena vacia", - "string_exceed_min_length": "Debe contener al menos {0} caracteres", - "string_exceed_max_length": "No debe contener mas de {0} caracteres", - "string_invalid_format": "Formato invalido ({0})", - "number_nan": "No es un numero", - "number_not_integer": "No es un numero entero", - "number_not_double": "No es un numero real", - "number_exceed_min": "Debe ser mayor que {0}", - "number_exceed_max": "Debe ser menot que {0}", - "number_wrong_step": "Debe ser multiplo de {0}", - "datetime_invalid": "Formato de fecha invalido ({0})", - "datetime_exceed_min": "Debe ser posterior a {0}", - "datetime_exceed_max": "Debe ser anterior a {0}" - } -}}); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/fr.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/fr.js deleted file mode 100644 index 723730009..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/fr.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * French translation by Damien "Mistic" Sorel - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Ajouter une règle", - "add_group": "Ajouter un groupe", - "delete_rule": "Supprimer", - "delete_group": "Supprimer", - - "condition_and": "ET", - "condition_or": "OU", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "égal", - "not_equal": "non égal", - "in": "dans", - "not_in": "pas dans", - "less": "inférieur", - "less_or_equal": "inférieur ou égal", - "greater": "supérieur", - "greater_or_equal": "supérieur ou égal", - "between": "entre", - "begins_with": "commence par", - "not_begins_with": "ne commence pas par", - "contains": "contient", - "not_contains": "ne contient pas", - "ends_with": "finit par", - "not_ends_with": "ne finit pas par", - "is_empty": "est vide", - "is_not_empty": "n'est pas vide", - "is_null": "est nul", - "is_not_null": "n'est pas nul" - }, - - "errors": { - "no_filter": "Aucun filtre sélectionné", - "empty_group": "Le groupe est vide", - "radio_empty": "Pas de valeur selectionnée", - "checkbox_empty": "Pas de valeur selectionnée", - "select_empty": "Pas de valeur selectionnée", - "string_empty": "Valeur vide", - "string_exceed_min_length": "Doit contenir au moins {0} caractères", - "string_exceed_max_length": "Ne doit pas contenir plus de {0} caractères", - "string_invalid_format": "Format invalide ({0})", - "number_nan": "N'est pas un nombre", - "number_not_integer": "N'est pas un entier", - "number_not_double": "N'est pas un nombre réel", - "number_exceed_min": "Doit être plus grand que {0}", - "number_exceed_max": "Doit être plus petit que {0}", - "number_wrong_step": "Doit être un multiple de {0}", - "datetime_invalid": "Fomat de date invalide ({0})", - "datetime_exceed_min": "Doit être après {0}", - "datetime_exceed_max": "Doit être avant {0}" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/it.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/it.js deleted file mode 100644 index 87a100b16..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/it.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * Italian translation - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Aggiungi regola", - "add_group": "Aggiungi gruppo", - "delete_rule": "Elimina", - "delete_group": "Elimina", - - "condition_and": "E", - "condition_or": "O", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "uguale", - "not_equal": "non uguale", - "in": "in", - "not_in": "non in", - "less": "minore", - "less_or_equal": "minore o uguale", - "greater": "maggiore", - "greater_or_equal": "maggiore o uguale", - "begins_with": "inizia con", - "not_begins_with": "non inizia con", - "contains": "contiene", - "not_contains": "non contiene", - "ends_with": "finisce con", - "not_ends_with": "non finisce con", - "is_empty": "è vuoto", - "is_not_empty": "non è vuoto", - "is_null": "è nullo", - "is_not_null": "non è nullo" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/nl.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/nl.js deleted file mode 100644 index 3a591f1fb..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/nl.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * Dutch translation by "Roywcm" - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Nieuwe regel", - "add_group": "Nieuwe groep", - "delete_rule": "Verwijder", - "delete_group": "Verwijder", - - "condition_and": "EN", - "condition_or": "OF", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "gelijk", - "not_equal": "niet gelijk", - "in": "in", - "not_in": "niet in", - "less": "minder", - "less_or_equal": "minder of gelijk", - "greater": "groter", - "greater_or_equal": "groter of gelijk", - "between": "tussen", - "begins_with": "begint met", - "not_begins_with": "begint niet met", - "contains": "bevat", - "not_contains": "bevat niet", - "ends_with": "eindigt met", - "not_ends_with": "eindigt niet met", - "is_empty": "is leeg", - "is_not_empty": "is niet leeg", - "is_null": "is null", - "is_not_null": "is niet null" - }, - - "errors": { - "no_filter": "Geen filter geselecteerd", - "empty_group": "De groep is leeg", - "radio_empty": "Geen waarde geselecteerd", - "checkbox_empty": "Geen waarde geselecteerd", - "select_empty": "Geen waarde geselecteerd", - "string_empty": "Lege waarde", - "string_exceed_min_length": "Dient minstens {0} karakters te bevatten", - "string_exceed_max_length": "Dient niet meer dan {0} karakters te bevatten", - "string_invalid_format": "Ongeldig format ({0})", - "number_nan": "Niet een nummer", - "number_not_integer": "Geen geheel getal", - "number_not_double": "Geen echt nummer", - "number_exceed_min": "Dient groter te zijn dan {0}", - "number_exceed_max": "Dient lager te zijn dan {0}", - "number_wrong_step": "Dient een veelvoud te zijn van {0}", - "datetime_invalid": "Ongeldige datumformat ({0})", - "datetime_exceed_min": "Dient na {0}", - "datetime_exceed_max": "Dient voor {0}" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pl.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pl.js deleted file mode 100644 index 475d25e2e..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pl.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * Polish translation by Artur Smolarek - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Dodaj regułę", - "add_group": "Dodaj grupę", - "delete_rule": "Usuń", - "delete_group": "Usuń", - - "condition_and": "AND", - "condition_or": "OR", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "równa się", - "not_equal": "jest różne od", - "in": "zawiera", - "not_in": "nie zawiera", - "less": "mniejsze", - "less_or_equal": "mniejsze lub równe", - "greater": "większe", - "greater_or_equal": "większe lub równe", - "between": "pomiędzy", - "begins_with": "rozpoczyna się od", - "not_begins_with": "nie rozpoczyna się od", - "contains": "zawiera", - "not_contains": "nie zawiera", - "ends_with": "kończy się na", - "not_ends_with": "nie kończy się na", - "is_empty": "jest puste", - "is_not_empty": "nie jest puste", - "is_null": "jest niezdefiniowane", - "is_not_null": "nie jest niezdefiniowane" - }, - - "errors": { - "no_filter": "Nie wybrano żadnego filtra", - "empty_group": "Grupa jest pusta", - "radio_empty": "Nie wybrano wartości", - "checkbox_empty": "Nie wybrano wartości", - "select_empty": "Nie wybrano wartości", - "string_empty": "Nie wpisano wartości", - "string_exceed_min_length": "Minimalna długość to {0} znaków", - "string_exceed_max_length": "Maksymalna długość to {0} znaków", - "string_invalid_format": "Nieprawidłowy format ({0})", - "number_nan": "To nie jest liczba", - "number_not_integer": "To nie jest liczba całkowita", - "number_not_double": "To nie jest liczba rzeczywista", - "number_exceed_min": "Musi być większe niż {0}", - "number_exceed_max": "Musi być mniejsze niż {0}", - "number_wrong_step": "Musi być wielokrotnością {0}", - "datetime_invalid": "Nieprawidłowy format daty ({0})", - "datetime_exceed_min": "Musi być po {0}", - "datetime_exceed_max": "Musi być przed {0}" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pt-BR.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pt-BR.js deleted file mode 100644 index 1aeed08e2..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pt-BR.js +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * jQuery QueryBuilder - * Portuguese Brazilian translation by Leandro Gehlen (leandrogehlen@gmail.com) - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Nova Regra", - "add_group": "Novo Gruop", - "delete_rule": "Excluir", - "delete_group": "Excluir", - - "condition_and": "E", - "condition_or": "OU", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "Igual", - "not_equal": "Diferente", - "in": "Contido", - "not_in": "Não contido", - "less": "Menor", - "less_or_equal": "Menor ou igual", - "greater": "Maior", - "greater_or_equal": "Maior ou igual", - "between": "entre", - "begins_with": "Iniciando com", - "not_begins_with": "Não iniciando com", - "contains": "Contém", - "not_contains": "Não contém", - "ends_with": "Terminando com", - "not_ends_with": "Terminando sem", - "is_empty": "É vazio", - "is_not_empty": "Não é vazio", - "is_null": "É nulo", - "is_not_null": "Não é nulo" - }, - - "errors": { - "no_filter": "Nenhum filtro selecionado", - "empty_group": "O grupo está vazio", - "radio_empty": "Nenhum valor selecionado", - "checkbox_empty": "Nenhum valor selecionado", - "select_empty": "Nenhum valor selecionado", - "string_empty": "Valor vazio", - "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres", - "string_exceed_max_length": "É necessário conterm mais de {0} caracteres", - "string_invalid_format": "Formato inválido ({0})", - "number_nan": "Não é um número", - "number_not_integer": "Não é um número inteiro", - "number_not_double": "Não é um número real", - "number_exceed_min": "É necessário ser maior que {0}", - "number_exceed_max": "É necessário ser menor que {0}", - "number_wrong_step": "É necessário ser múltiplo de {0}", - "datetime_invalid": "Formato de data inválido ({0})", - "datetime_exceed_min": "É necessário ser superior a {0}", - "datetime_exceed_max": "É necessário ser inferior a {0}" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/ro.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/ro.js deleted file mode 100644 index 477d8cf6c..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/ro.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * jQuery QueryBuilder - * Romanian translation by ArianServ - */ - -jQuery.fn.queryBuilder.defaults.set({ lang: { - "add_rule": "Adaugă regulă", - "add_group": "Adaugă grup", - "delete_rule": "Şterge", - "delete_group": "Şterge", - - "condition_and": "ŞI", - "condition_or": "SAU", - - "filter_select_placeholder": "------", - - "operators": { - "equal": "egal", - "not_equal": "diferit", - "in": "în", - "not_in": "nu în", - "less": "mai puţin", - "less_or_equal": "mai puţin sau egal", - "greater": "mai mare", - "greater_or_equal": "mai mare sau egal", - "begins_with": "începe cu", - "not_begins_with": "nu începe cu", - "contains": "conţine", - "not_contains": "nu conţine", - "ends_with": "se termină cu", - "not_ends_with": "nu se termină cu", - "is_empty": "este gol", - "is_not_empty": "nu este gol", - "is_null": "e nul", - "is_not_null": "nu e nul" - } -}}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.css b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.css deleted file mode 100644 index a194d84ef..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.css +++ /dev/null @@ -1,131 +0,0 @@ -/*! - * jQuery QueryBuilder 1.4.1 - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ -.query-builder .rule-container, -.query-builder .rules-group-container, -.query-builder .rule-placeholder { - margin:4px 0; - border-radius:5px; - padding:5px; - border:1px solid #eee; - background:#fff; - background:rgba(255, 255, 255, 0.9); -} - -.query-builder .rules-group-container { - padding:10px 10px 5px 10px; - border:1px solid #DCC896; - background:#FCF9ED; - background:rgba(250, 240, 210, 0.5); -} - .query-builder .rules-group-header { - margin-bottom:10px; - } - .query-builder .rules-group-header input[name$=_cond] { - display:none; - } - .query-builder .rules-list { - list-style:none; - padding:0 0 0 20px; - margin:0; - } - -.query-builder .rule-container {} - .query-builder .rule-container>div:not(.rule-header) { - display:inline-block; - margin:0 5px 0 0; - vertical-align:top; - } - .query-builder .rule-value-container:not(:empty) { - border-left:1px solid #ddd; - padding-left:5px; - } - .query-builder .rule-value-container label { - margin-bottom:0; - } - .query-builder .rule-value-container label.block { - display:block; - } - .query-builder .rule-container select, - .query-builder .rule-container input[type=text], - .query-builder .rule-container input[type=number] { - padding:1px; - } - -.query-builder .has-error { - background:#fdd; - border-color:#f99; -} - -.query-builder .error-container { - display:none !important; - cursor:help; - color:red; -} - -.query-builder .has-error .error-container { - display:inline-block !important; -} - -.query-builder .rules-list>* { - position:relative; -} - .query-builder .rules-list>*:before, - .query-builder .rules-list>*:after { - content:''; - position:absolute; - left:-15px; - width:15px; - height:calc(50% + 4px); - border-color:#ccc; - border-style:solid; - } - - .query-builder .rules-list>*:before { - top:-2px; - border-width:0 0 2px 2px; - } - .query-builder .rules-list>*:after { - top:50%; - border-width:0 0 0 2px; - } - - .query-builder .rules-list>*:first-child:before { - top:-12px; - height:calc(50% + 14px); - } - .query-builder .rules-list>*:last-child:before { - border-radius:0 0 0 4px; - } - .query-builder .rules-list>*:last-child:after { - display:none; - } -.query-builder .tooltip-inner { - color:#fdd !important; -} -.query-builder p.filter-description { - margin:5px 0 0 0; - background:#D9EDF7; - border:1px solid #BCE8F1; - color:#31708F; - border-radius:4px; - padding:2px 5px; - font-size:0.8em; -} -.query-builder .drag-handle { - cursor:move; - display:inline-block; - vertical-align:middle; - margin-left:5px; -} - -.query-builder .dragged { - opacity:0.5; -} - -.query-builder .rule-placeholder { - border:1px dashed #bbb; - opacity:0.7; -} \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.js deleted file mode 100644 index b3486bb60..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.js +++ /dev/null @@ -1,2289 +0,0 @@ -/*! - * jQuery QueryBuilder 1.4.1 - * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -// Modules: bt-selectpicker, bt-tooltip-errors, filter-description, mongodb-support, sortable, sql-support -(function(root, factory) { - if (typeof define === 'function' && define.amd) { - define(['jquery', 'microevent', 'jQuery.extendext'], factory); - } - else { - factory(root.jQuery, root.MicroEvent); - } -}(this, function($, MicroEvent) { - "use strict"; - - var types = [ - 'string', - 'integer', - 'double', - 'date', - 'time', - 'datetime' - ], - internalTypes = [ - 'string', - 'number', - 'datetime' - ], - inputs = [ - 'text', - 'textarea', - 'radio', - 'checkbox', - 'select' - ]; - - - var QueryBuilder = function($el, options) { - this.$el = $el; - this.init(options); - }; - - MicroEvent.mixin(QueryBuilder); - - - QueryBuilder.DEFAULTS = { - filters: [], - - plugins: null, - - onValidationError: null, - onAfterAddGroup: null, - onAfterAddRule: null, - - display_errors: true, - allow_groups: -1, - allow_empty: false, - conditions: ['AND', 'OR'], - default_condition: 'AND', - - default_rule_flags: { - filter_readonly: false, - operator_readonly: false, - value_readonly: false, - no_delete: false - }, - - template: { - group: null, - rule: null - }, - - lang: { - "add_rule": 'Add rule', - "add_group": 'Add group', - "delete_rule": 'Delete', - "delete_group": 'Delete', - - "condition_and": 'AND', - "condition_or": 'OR', - - "filter_select_placeholder": '------', - - "operators": { - "equal": "equal", - "not_equal": "not equal", - "in": "in", - "not_in": "not in", - "less": "less", - "less_or_equal": "less or equal", - "greater": "greater", - "greater_or_equal": "greater or equal", - "between": "between", - "begins_with": "begins with", - "not_begins_with": "doesn't begin with", - "contains": "contains", - "not_contains": "doesn't contain", - "ends_with": "ends with", - "not_ends_with": "doesn't end with", - "is_empty": "is empty", - "is_not_empty": "is not empty", - "is_null": "is null", - "is_not_null": "is not null" - }, - - "errors": { - "no_filter": "No filter selected", - "empty_group": "The group is empty", - "radio_empty": "No value selected", - "checkbox_empty": "No value selected", - "select_empty": "No value selected", - "string_empty": "Empty value", - "string_exceed_min_length": "Must contain at least {0} characters", - "string_exceed_max_length": "Must not contain more than {0} characters", - "string_invalid_format": "Invalid format ({0})", - "number_nan": "Not a number", - "number_not_integer": "Not an integer", - "number_not_double": "Not a real number", - "number_exceed_min": "Must be greater than {0}", - "number_exceed_max": "Must be lower than {0}", - "number_wrong_step": "Must be a multiple of {0}", - "datetime_invalid": "Invalid date format ({0})", - "datetime_exceed_min": "Must be after {0}", - "datetime_exceed_max": "Must be before {0}" - } - }, - - operators: [ - {type: 'equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'not_equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'not_in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, - {type: 'less', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'less_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'greater_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, - {type: 'between', accept_values: 2, apply_to: ['number', 'datetime']}, - {type: 'begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_begins_with', accept_values: 1, apply_to: ['string']}, - {type: 'contains', accept_values: 1, apply_to: ['string']}, - {type: 'not_contains', accept_values: 1, apply_to: ['string']}, - {type: 'ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'not_ends_with', accept_values: 1, apply_to: ['string']}, - {type: 'is_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_not_empty', accept_values: 0, apply_to: ['string']}, - {type: 'is_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']}, - {type: 'is_not_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']} - ], - - icons: { - add_group: 'glyphicon glyphicon-plus-sign', - add_rule: 'glyphicon glyphicon-plus', - remove_group: 'glyphicon glyphicon-remove', - remove_rule: 'glyphicon glyphicon-remove', - error: 'glyphicon glyphicon-warning-sign' - } - }; - - - QueryBuilder.plugins = {}; - - /** - * Define a new plugin - * @param {string} - * @param {function} - */ - QueryBuilder.define = function(name, fct) { - QueryBuilder.plugins[name] = fct; - }; - - /** - * Add new methods - * @param {object} - */ - QueryBuilder.extend = function(methods) { - $.extend(QueryBuilder.prototype, methods); - }; - - /** - * Init plugins for an instance - */ - QueryBuilder.prototype.initPlugins = function() { - if (!this.settings.plugins) { - return; - } - - var that = this, - queue = {}; - - if ($.isArray(this.settings.plugins)) { - $.each(this.settings.plugins, function(i, plugin) { - queue[plugin] = {}; - }); - } - else { - $.each(this.settings.plugins, function(plugin, options) { - queue[plugin] = options; - }); - } - - $.each(queue, function(plugin, options) { - if (plugin in QueryBuilder.plugins) { - QueryBuilder.plugins[plugin].call(that, options); - } - else { - $.error('Unable to find plugin "' + plugin +'"'); - } - }); - }; - - - /** - * Init the builder - */ - QueryBuilder.prototype.init = function(options) { - // PROPERTIES - this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); - this.status = { - group_id: 0, - rule_id: 0, - generatedId: false, - has_optgroup: false - }; - - // "allow_groups" changed in 1.3.1 from boolean to int - if (this.settings.allow_groups === false) { - this.settings.allow_groups = 0; - } - else if (this.settings.allow_groups === true) { - this.settings.allow_groups = -1; - } - - this.filters = this.settings.filters; - this.lang = this.settings.lang; - this.icons = this.settings.icons; - this.operators = this.settings.operators; - this.template = this.settings.template; - - if (this.template.group === null) { - this.template.group = this.getGroupTemplate; - } - if (this.template.rule === null) { - this.template.rule = this.getRuleTemplate; - } - - // CHECK FILTERS - if (!this.filters || this.filters.length < 1) { - $.error('Missing filters list'); - } - this.checkFilters(); - - // ensure we have a container id - if (!this.$el.attr('id')) { - this.$el.attr('id', 'qb_'+Math.floor(Math.random()*99999)); - this.status.generatedId = true; - } - this.$el_id = this.$el.attr('id'); - - this.$el.addClass('query-builder'); - - // INIT - this.bindEvents(); - - this.initPlugins(); - - this.trigger('afterInit'); - - if (options.rules) { - this.setRules(options.rules); - } - else { - this.addGroup(this.$el); - } - }; - - /** - * Destroy the plugin - */ - QueryBuilder.prototype.destroy = function() { - this.trigger('beforeDestroy'); - - if (this.status.generatedId) { - this.$el.removeAttr('id'); - } - - this.$el.empty() - .off('click.queryBuilder change.queryBuilder') - .removeClass('query-builder') - .removeData('queryBuilder'); - }; - - /** - * Reset the plugin - */ - QueryBuilder.prototype.reset = function() { - this.status.group_id = 1; - this.status.rule_id = 0; - - this.$el.find('>.rules-group-container>.rules-group-body>.rules-list').empty(); - - this.addRule(this.$el.find('>.rules-group-container')); - - this.trigger('afterReset'); - }; - - /** - * Clear the plugin - */ - QueryBuilder.prototype.clear = function() { - this.status.group_id = 0; - this.status.rule_id = 0; - - this.$el.empty(); - - this.trigger('afterClear'); - }; - - /** - * Get an object representing current rules - * @return {object} - */ - QueryBuilder.prototype.getRules = function() { - this.clearErrors(); - - var $group = this.$el.find('>.rules-group-container'), - that = this; - - var rules = (function parse($group) { - var out = {}, - $elements = $group.find('>.rules-group-body>.rules-list>*'); - - out.condition = that.getGroupCondition($group); - out.rules = []; - - for (var i=0, l=$elements.length; i 1)) { - that.triggerValidationError(['empty_group'], $group, null, null, null); - return {}; - } - - return out; - }($group)); - - return this.change('getRules', rules); - }; - - /** - * Set rules from object - * @param data {object} - */ - QueryBuilder.prototype.setRules = function(data) { - this.clear(); - - if (!data || !data.rules || (data.rules.length===0 && !this.settings.allow_empty)) { - $.error('Incorrect data object passed'); - } - - data = this.change('setRules', data); - - var $container = this.$el, - that = this; - - (function add(data, $container){ - var $group = that.addGroup($container, false); - if ($group === null) { - return; - } - - var $buttons = $group.find('>.rules-group-header [name$=_cond]'); - - if (data.condition === undefined) { - data.condition = that.settings.default_condition; - } - - for (var i=0, l=that.settings.conditions.length; i0) { - if (that.settings.allow_groups !== -1 && that.settings.allow_groups < $group.data('queryBuilder').level) { - that.reset(); - $.error(fmt('No more than {0} groups are allowed', that.settings.allow_groups)); - } - else { - add(rule, $group); - } - } - else { - if (rule.id === undefined) { - $.error('Missing rule field id'); - } - if (rule.value === undefined) { - rule.value = ''; - } - if (rule.operator === undefined) { - rule.operator = 'equal'; - } - - var $rule = that.addRule($group); - if ($rule === null) { - return; - } - - var filter = that.getFilterById(rule.id), - operator = that.getOperatorByType(rule.operator); - - $rule.find('.rule-filter-container [name$=_filter]').val(rule.id).trigger('change'); - $rule.find('.rule-operator-container [name$=_operator]').val(rule.operator).trigger('change'); - - if (operator.accept_values !== 0) { - that.setRuleValue($rule, rule.value, filter, operator); - } - - that.applyRuleFlags($rule, rule); - } - }); - - }(data, $container)); - }; - - - /** - * Checks the configuration of each filter - */ - QueryBuilder.prototype.checkFilters = function() { - var definedFilters = [], - that = this; - - $.each(this.filters, function(i, filter) { - if (!filter.id) { - $.error('Missing filter id: '+ i); - } - if (definedFilters.indexOf(filter.id) != -1) { - $.error('Filter already defined: '+ filter.id); - } - definedFilters.push(filter.id); - - if (!filter.type) { - $.error('Missing filter type: '+ filter.id); - } - if (types.indexOf(filter.type) == -1) { - $.error('Invalid type: '+ filter.type); - } - - if (!filter.input) { - filter.input = 'text'; - } - else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) { - $.error('Invalid input: '+ filter.input); - } - - if (!filter.field) { - filter.field = filter.id; - } - if (!filter.label) { - filter.label = filter.field; - } - - that.status.has_optgroup|= !!filter.optgroup; - if (!filter.optgroup) { - filter.optgroup = null; - } - - switch (filter.type) { - case 'string': - filter.internalType = 'string'; - break; - case 'integer': case 'double': - filter.internalType = 'number'; - break; - case 'date': case 'time': case 'datetime': - filter.internalType = 'datetime'; - break; - } - - switch (filter.input) { - case 'radio': case 'checkbox': - if (!filter.values || filter.values.length < 1) { - $.error('Missing values for filter: '+ filter.id); - } - break; - } - }); - - // group filters with same optgroup, preserving declaration order when possible - if (this.status.has_optgroup) { - var optgroups = [], - filters = []; - - $.each(this.filters, function(i, filter) { - var idx; - - if (filter.optgroup) { - idx = optgroups.lastIndexOf(filter.optgroup); - - if (idx == -1) { - idx = optgroups.length; - } - } - else { - idx = optgroups.length; - } - - optgroups.splice(idx, 0, filter.optgroup); - filters.splice(idx, 0, filter); - }); - - this.filters = filters; - } - - this.trigger('afterCheckFilters'); - }; - - /** - * Add all events listeners - */ - QueryBuilder.prototype.bindEvents = function() { - var that = this; - - // group condition change - this.$el.on('change.queryBuilder', '.rules-group-header [name$=_cond]', function() { - var $this = $(this); - - if ($this.is(':checked')) { - $this.parent().addClass('active').siblings().removeClass('active'); - } - }); - - // rule filter change - this.$el.on('change.queryBuilder', '.rule-filter-container [name$=_filter]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleFilter($rule, $this.val()); - }); - - // rule operator change - this.$el.on('change.queryBuilder', '.rule-operator-container [name$=_operator]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleOperator($rule, $this.val()); - }); - - // add rule button - this.$el.on('click.queryBuilder', '[data-add=rule]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addRule($group); - }); - - // delete rule button - this.$el.on('click.queryBuilder', '[data-delete=rule]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.deleteRule($rule); - }); - - if (this.settings.allow_groups !== 0) { - // add group button - this.$el.on('click.queryBuilder', '[data-add=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addGroup($group); - }); - - // delete group button - this.$el.on('click.queryBuilder', '[data-delete=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.deleteGroup($group); - }); - } - }; - - /** - * Add a new rules group - * @param $parent {jQuery} - * @param addRule {bool} (optional - add a default empty rule) - * @return $group {jQuery} - */ - QueryBuilder.prototype.addGroup = function($parent, addRule) { - var group_id = this.nextGroupId(), - level = (($parent.data('queryBuilder') || {}).level || 0) + 1, - $container = level===1 ? $parent : $parent.find('>.rules-group-body>.rules-list'), - $group = $(this.template.group.call(this, group_id, level)); - - $group.data('queryBuilder', {level:level}); - - var e = $.Event('addGroup.queryBuilder', { - group_id: group_id, - level: level, - addRule: addRule, - group: $group, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($group); - - if (this.settings.onAfterAddGroup) { - this.settings.onAfterAddGroup.call(this, $group); - } - - this.trigger('afterAddGroup', $group); - - if (addRule === undefined || addRule === true) { - this.addRule($group); - } - - return $group; - }; - - /** - * Tries to delete a group. The group is not deleted if at least one rule is no_delete. - * @param $group {jQuery} - * @return {boolean} true if the group has been deleted - */ - QueryBuilder.prototype.deleteGroup = function($group) { - if ($group[0].id == this.$el_id + '_group_0') { - return; - } - - var e = $.Event('deleteGroup.queryBuilder', { - group_id: $group[0].id, - group: $group, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteGroup', $group); - - var that = this, - keepGroup = false; - - $group.find('>.rules-group-body>.rules-list>*').each(function() { - var $element = $(this); - - if ($element.hasClass('rule-container')) { - if ($element.data('queryBuilder').flags.no_delete) { - keepGroup = true; - } - else { - $element.remove(); - } - } - else { - keepGroup|= !that.deleteGroup($element); - } - }); - - if (!keepGroup) { - $group.remove(); - } - - return !keepGroup; - }; - - /** - * Add a new rule - * @param $parent {jQuery} - * @return $rule {jQuery} - */ - QueryBuilder.prototype.addRule = function($parent) { - var rule_id = this.nextRuleId(), - $container = $parent.find('>.rules-group-body>.rules-list'), - $rule = $(this.template.rule.call(this, rule_id)), - $filterSelect = $(this.getRuleFilterSelect(rule_id)); - - $rule.data('queryBuilder', {flags: {}}); - - var e = $.Event('addRule.queryBuilder', { - rule_id: rule_id, - rule: $rule, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($rule); - $rule.find('.rule-filter-container').append($filterSelect); - - if (this.settings.onAfterAddRule) { - this.settings.onAfterAddRule.call(this, $rule); - } - - this.trigger('afterAddRule', $rule); - - return $rule; - }; - - /** - * Delete a rule. - * @param $rule {jQuery} - * @return {boolean} true if the rule has been deleted - */ - QueryBuilder.prototype.deleteRule = function($rule) { - var e = $.Event('deleteRule.queryBuilder', { - rule_id: $rule[0].id, - rule: $rule, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteRule', $rule); - - $rule.remove(); - return true; - }; - - /** - * Create operators for a rule - * @param $rule {jQuery} (
  • element) - * @param filter {object} - */ - QueryBuilder.prototype.createRuleInput = function($rule, filter) { - var $valueContainer = $rule.find('.rule-value-container').empty(); - - if (filter === null) { - return; - } - - var operator = this.getOperatorByType(this.getRuleOperator($rule)); - - if (operator.accept_values === 0) { - return; - } - - var $inputs = $(); - - for (var i=0; i 0) $valueContainer.append(' , '); - $valueContainer.append($ruleInput); - $inputs = $inputs.add($ruleInput); - } - - $valueContainer.show(); - - if (filter.onAfterCreateRuleInput) { - filter.onAfterCreateRuleInput.call(this, $rule, filter); - } - - if (filter.plugin) { - $inputs[filter.plugin](filter.plugin_config || {}); - } - - if (filter.default_value !== undefined) { - this.setRuleValue($rule, filter.default_value, filter, operator); - } - - this.trigger('afterCreateRuleInput', $rule, filter, operator); - }; - - /** - * Perform action when rule's filter is changed - * @param $rule {jQuery} (
  • element) - * @param filterId {string} - */ - QueryBuilder.prototype.updateRuleFilter = function($rule, filterId) { - var filter = filterId != '-1' ? this.getFilterById(filterId) : null; - - this.createRuleOperators($rule, filter); - this.createRuleInput($rule, filter); - - $rule.data('queryBuilder').filter = filter; - - this.trigger('afterUpdateRuleFilter', $rule, filter); - }; - - /** - * Update main visibility when rule operator changes - * @param $rule {jQuery} (
  • element) - * @param operatorType {string} - */ - QueryBuilder.prototype.updateRuleOperator = function($rule, operatorType) { - var $valueContainer = $rule.find('.rule-value-container'), - filter = this.getFilterById(this.getRuleFilter($rule)), - operator = this.getOperatorByType(operatorType); - - if (operator.accept_values === 0) { - $valueContainer.hide(); - } - else { - $valueContainer.show(); - - var previousOperator = $rule.data('queryBuilder').operator; - - if ($valueContainer.is(':empty') || operator.accept_values != previousOperator.accept_values) { - this.createRuleInput($rule, filter); - } - } - - $rule.data('queryBuilder').operator = operator; - - if (filter.onAfterChangeOperator) { - filter.onAfterChangeOperator.call(this, $rule, filter, operator); - } - - this.trigger('afterChangeOperator', $rule, filter, operator); - }; - - /** - * Check if a value is correct for a filter - * @param $rule {jQuery} (
  • element) - * @param value {string|string[]|undefined} - * @param filter {object} - * @param operator {object} - * @return {array|true} - */ - QueryBuilder.prototype.validateValue = function($rule, value, filter, operator) { - var validation = filter.validation || {}, - result = true; - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - if (validation.callback) { - result = validation.callback.call(this, value, filter, operator, $rule); - return this.change('validateValue', result, $rule, value, filter, operator); - } - - for (var i=0; i validation.max) { - result = ['string_exceed_max_length', validation.max]; - break; - } - } - if (validation.format) { - if (!(validation.format.test(value[i]))) { - result = ['string_invalid_format', validation.format]; - break; - } - } - break; - - case 'number': - if (isNaN(value[i])) { - result = ['number_nan']; - break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; - break; - } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; - break; - } - } - if (validation.min !== undefined) { - if (value[i] < validation.min) { - result = ['number_exceed_min', validation.min]; - break; - } - } - if (validation.max !== undefined) { - if (value[i] > validation.max) { - result = ['number_exceed_max', validation.max]; - break; - } - } - if (validation.step !== undefined) { - var v = value[i]/validation.step; - if (parseInt(v) != v) { - result = ['number_wrong_step', validation.step]; - break; - } - } - break; - - case 'datetime': - // we need MomentJS - if (window.moment && validation.format) { - var datetime = moment(value[i], validation.format); - if (!datetime.isValid()) { - result = ['datetime_invalid']; - break; - } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = ['datetime_exceed_min', validation.min]; - break; - } - } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = ['datetime_exceed_max', validation.max]; - break; - } - } - } - } - break; - } - } - - if (result !== true) { - break; - } - } - - return this.change('validateValue', result, $rule, value, filter, operator); - }; - - /** - * Remove 'has-error' from everything - */ - QueryBuilder.prototype.clearErrors = function() { - this.$el.find('.has-error').removeClass('has-error'); - }; - - /** - * Trigger a validation error event with custom params - * @param error {array} - * @param $target {jQuery} - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.triggerValidationError = function(error, $target, value, filter, operator) { - if (!$.isArray(error)) { - error = [error]; - } - - if (filter && filter.onValidationError) { - filter.onValidationError.call(this, $target, error, value, filter, operator); - } - if (this.settings.onValidationError) { - this.settings.onValidationError.call(this, $target, error, value, filter, operator); - } - - var e = $.Event('validationError.queryBuilder', { - error: error, - filter: filter, - operator: operator, - value: value, - targetRule: $target[0], - builder: this - }); - - this.$el.trigger(e); - - if (this.settings.display_errors && !e.isDefaultPrevented()) { - // translate the text without modifying event array - var errorLoc = $.extend([], error, [ - this.lang.errors[error[0]] || error[0] - ]); - - $target.addClass('has-error'); - var $error = $target.find('.error-container').eq(0); - $error.attr('title', fmt.apply(null, errorLoc)); - } - - this.trigger('validationError', $target, error); - }; - - - /** - * Returns an incremented group ID - * @return {string} - */ - QueryBuilder.prototype.nextGroupId = function() { - return this.$el_id + '_group_' + (this.status.group_id++); - }; - - /** - * Returns an incremented rule ID - * @return {string} - */ - QueryBuilder.prototype.nextRuleId = function() { - return this.$el_id + '_rule_' + (this.status.rule_id++); - }; - - /** - * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} - */ - QueryBuilder.prototype.getOperators = function(filter) { - if (typeof filter === 'string') { - filter = this.getFilterById(filter); - } - - var result = []; - - for (var i=0, l=this.operators.length; i element) - * @return {string} - */ - QueryBuilder.prototype.getGroupCondition = function($group) { - return $group.find('>.rules-group-header [name$=_cond]:checked').val(); - }; - - /** - * Returns the selected filter of a rule - * @param $rule {jQuery} (
  • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleFilter = function($rule) { - return $rule.find('.rule-filter-container [name$=_filter]').val(); - }; - - /** - * Returns the selected operator of a rule - * @param $rule {jQuery} (
  • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleOperator = function($rule) { - return $rule.find('.rule-operator-container [name$=_operator]').val(); - }; - - /** - * Returns rule value - * @param $rule {jQuery} (
  • element) - * @param filter {object} (optional - current rule filter) - * @param operator {object} (optional - current rule operator) - * @return {string|string[]|undefined} - */ - QueryBuilder.prototype.getRuleValue = function($rule, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - var value = [], tmp, - $value = $rule.find('.rule-value-container'); - - for (var i=0; i element) - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.setRuleValue = function($rule, value, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - this.trigger('beforeSetRuleValue', $rule, value, filter, operator); - - if (filter.valueSetter) { - filter.valueSetter.call(this, $rule, value, filter, operator); - } - else { - var $value = $rule.find('.rule-value-container'); - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - for (var i=0; i element) - * @param rule {object} - */ - QueryBuilder.prototype.applyRuleFlags = function($rule, rule) { - var flags = this.getRuleFlags(rule); - $rule.data('queryBuilder').flags = flags; - - if (flags.filter_readonly) { - $rule.find('[name$=_filter]').prop('disabled', true); - } - if (flags.operator_readonly) { - $rule.find('[name$=_operator]').prop('disabled', true); - } - if (flags.value_readonly) { - $rule.find('[name*=_value_]').prop('disabled', true); - } - if (flags.no_delete) { - $rule.find('[data-delete=rule]').remove(); - } - - this.trigger('afterApplyRuleFlags', $rule, rule, flags); - }; - - - /** - * Returns group HTML - * @param group_id {string} - * @param level {int} - * @return {string} - */ - QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { - var h = '\ -
    \ -
    \ -
    \ - \ - '+ (this.settings.allow_groups===-1 || this.settings.allow_groups>=level ? - '' - :'') +' \ - '+ (level>1 ? - '' - : '') +' \ -
    \ -
    \ - '+ this.getGroupConditions(group_id) +' \ -
    \ - '+ (this.settings.display_errors ? - '
    ' - :'') +'\ -
    \ -
    \ -
      \ -
      \ -
      '; - - return this.change('getGroupTemplate', h, level); - }; - - /** - * Returns group conditions HTML - * @param group_id {string} - * @return {string} - */ - QueryBuilder.prototype.getGroupConditions = function(group_id) { - var h = ''; - - for (var i=0, l=this.settings.conditions.length; i \ - '+ label +' \ - '; - } - - return this.change('getGroupConditions', h); - }; - - /** - * Returns rule HTML - * @param rule_id {string} - * @return {string} - */ - QueryBuilder.prototype.getRuleTemplate = function(rule_id) { - var h = '\ -
    • \ -
      \ -
      \ - \ -
      \ -
      \ - '+ (this.settings.display_errors ? - '
      ' - :'') +'\ -
      \ -
      \ -
      \ -
    • '; - - return this.change('getRuleTemplate', h); - }; - - /** - * Returns rule filter '; - h+= ''; - - $.each(this.filters, function(i, filter) { - if (optgroup != filter.optgroup) { - if (optgroup !== null) h+= ''; - optgroup = filter.optgroup; - if (optgroup !== null) h+= ''; - } - - h+= ''; - }); - - if (optgroup !== null) h+= ''; - h+= ''; - - return this.change('getRuleFilterSelect', h); - }; - - /** - * Returns rule operator '; - - for (var i=0, l=operators.length; i'+ label +''; - } - - h+= ''; - - return this.change('getRuleOperatorSelect', h); - }; - - /** - * Return the rule value HTML - * @param $rule {jQuery} - * @param filter {object} - * @param value_id {int} - * @return {string} - */ - QueryBuilder.prototype.getRuleInput = function($rule, filter, value_id) { - var validation = filter.validation || {}, - name = $rule[0].id +'_value_'+ value_id, - h = '', c; - - if (typeof filter.input === 'function') { - h = filter.input.call(this, $rule, filter, name); - } - else { - switch (filter.input) { - case 'radio': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'checkbox': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'select': - h+= ''; - break; - - case 'textarea': - h+= '";break;default:switch(c.internalType){case"number":h+='1&&$.error("Unable to initialize on multiple target");var b=this.data("queryBuilder"),c="object"==typeof a&&a||{};return b||"destroy"!=a?(b||this.data("queryBuilder",new j(this,c)),"string"==typeof a?b[a].apply(b,Array.prototype.slice.call(arguments,1)):this):this},$.fn.queryBuilder.defaults={set:function(a){$.extendext(!0,"replace",j.DEFAULTS,a)},get:function(a){var b=j.DEFAULTS;return a&&(b=b[a]),$.extend(!0,{},b)}},$.fn.queryBuilder.constructor=j,$.fn.queryBuilder.extend=j.extend,$.fn.queryBuilder.define=j.define,$.fn.queryBuilder.define("bt-selectpicker",function(a){$.fn.selectpicker&&$.fn.selectpicker.Constructor||$.error('Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'),a=$.extend({container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1},a||{}),this.on("afterAddRule",function(b){b.find(".rule-filter-container select").selectpicker(a)}),this.on("afterCreateRuleOperators",function(b){b.find(".rule-operator-container select").selectpicker(a)})}),$.fn.queryBuilder.define("bt-tooltip-errors",function(a){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||$.error('Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'),a=$.extend({placement:"right"},a||{}),this.on("getRuleTemplate",function(a){return a.replace('class="error-container"','class="error-container" data-toggle="tooltip"')}),this.on("validationError",function(b){b.find(".error-container").eq(0).tooltip(a).tooltip("hide").tooltip("fixTitle")})}),$.fn.queryBuilder.define("filter-description",function(a){a=$.extend({icon:"glyphicon glyphicon-info-sign",mode:"popover"},a||{}),"inline"===a.mode?this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("p.filter-description");c&&c.description?(0===d.length?(d=$('

      '),d.appendTo(b)):d.show(),d.html(' '+c.description)):d.hide()}):"popover"===a.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||$.error('Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length?(d=$(''),d.prependTo(b.find(".rule-actions")),d.popover({placement:"left",container:"body",html:!0}),d.on("mouseout",function(){d.popover("hide")})):d.show(),d.data("bs.popover").options.content=c.description,d.attr("aria-describedby")&&d.popover("show")):(d.hide(),d.data("bs.popover")&&d.popover("hide"))})):"bootbox"===a.mode&&(window.bootbox||$.error('Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length&&(d=$(''),d.prependTo(b.find(".rule-actions")),d.on("click",function(){bootbox.alert(d.data("description"))})),d.data("description",c.description)):d.hide()}))}),$.fn.queryBuilder.defaults.set({mongoOperators:{equal:function(a){return a[0]},not_equal:function(a){return{$ne:a[0]}},"in":function(a){return{$in:a}},not_in:function(a){return{$nin:a}},less:function(a){return{$lt:a[0]}},less_or_equal:function(a){return{$lte:a[0]}},greater:function(a){return{$gt:a[0]}},greater_or_equal:function(a){return{$gte:a[0]}},between:function(a){return{$gte:a[0],$lte:a[1]}},begins_with:function(a){return{$regex:"^"+e(a[0])}},not_begins_with:function(a){return{$regex:"^(?!"+e(a[0])+")"}},contains:function(a){return{$regex:e(a[0])}},not_contains:function(a){return{$regex:"^((?!"+e(a[0])+").)*$",$options:"s"}},ends_with:function(a){return{$regex:e(a[0])+"$"}},not_ends_with:function(a){return{$regex:"(?0)e.push(c(f));else{var g=b.settings.mongoOperators[f.operator],h=b.getOperatorByType(f.operator),i=[];void 0===g&&$.error("MongoDB operation unknown for operator "+f.operator),h.accept_values&&(f.value instanceof Array||(f.value=[f.value]),f.value.forEach(function(a){i.push(d(a,f.type))}));var j={};j[f.field]=g.call(b,i),e.push(j)}});var f={};return e.length>0&&(f["$"+a.condition.toLowerCase()]=e),f}(a)}}),$.fn.queryBuilder.define("sortable",function(a){a=$.extend({default_no_sortable:!1,icon:"glyphicon glyphicon-sort"},a||{}),this.on("afterInit",function(){$.event.props.push("dataTransfer");var a,b,c=this;this.$el.on("mouseover",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").attr("draggable",!0)}),this.$el.on("mouseout",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")}),this.$el.on("dragstart","[draggable]",function(c){c.stopPropagation(),c.dataTransfer.setData("text","drag"),b=$(c.target),a=$('
       
      '),a.css("min-height",b.height()),a.insertAfter(b),setTimeout(function(){b.hide()},0)}),this.$el.on("dragenter","[draggable]",function(b){b.preventDefault(),b.stopPropagation(),f(a,$(b.target))}),this.$el.on("dragover","[draggable]",function(a){a.preventDefault(),a.stopPropagation()}),this.$el.on("drop",function(a){a.preventDefault(),a.stopPropagation(),f(b,$(a.target))}),this.$el.on("dragend","[draggable]",function(d){d.preventDefault(),d.stopPropagation(),b.show(),a.remove(),b=a=null,c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")})}),this.on("getRuleFlags",function(b){return void 0===b.no_sortable&&(b.no_sortable=a.default_no_sortable),b}),this.on("afterApplyRuleFlags",function(a,b,c){c.no_sortable&&a.find(".drag-handle").remove()}),this.on("getGroupTemplate",function(b,c){if(c>1){var d=$(b);d.find(".group-conditions").after('
      '),b=d.prop("outerHTML")}return b}),this.on("getRuleTemplate",function(b){var c=$(b);return c.find(".rule-header").after('
      '),c.prop("outerHTML")})}),$.fn.queryBuilder.defaults.set({sqlOperators:{equal:"= ?",not_equal:"!= ?","in":{op:"IN(?)",list:!0,sep:", "},not_in:{op:"NOT IN(?)",list:!0,sep:", "},less:"< ?",less_or_equal:"<= ?",greater:"> ?",greater_or_equal:">= ?",between:{op:"BETWEEN ?",list:!0,sep:" AND "},begins_with:{op:"LIKE(?)",fn:function(a){return a+"%"}},not_begins_with:{op:"NOT LIKE(?)",fn:function(a){return a+"%"}},contains:{op:"LIKE(?)",fn:function(a){return"%"+a+"%"}},not_contains:{op:"NOT LIKE(?)",fn:function(a){return"%"+a+"%"}},ends_with:{op:"LIKE(?)",fn:function(a){return"%"+a}},not_ends_with:{op:"NOT LIKE(?)",fn:function(a){return"%"+a}},is_empty:'== ""',is_not_empty:'!= ""',is_null:"IS NULL",is_not_null:"IS NOT NULL"}}),$.fn.queryBuilder.extend({getSQL:function(a,b,c){c=void 0===c?this.getRules():c,a=a===!0||void 0===a?"question_mark":a,b=b||void 0===b?"\n":" "; -var e=this,f=1,h=[],i=function j(c){if(c.condition||(c.condition=e.settings.default_condition),-1===["AND","OR"].indexOf(c.condition.toUpperCase())&&$.error("Unable to build SQL query with "+c.condition+" condition"),!c.rules)return"";var i=[];return $.each(c.rules,function(c,k){if(k.rules&&k.rules.length>0)i.push("("+b+j(k)+b+")"+b);else{var l=e.getSqlOperator(k.operator),m=e.getOperatorByType(k.operator),n="";l===!1&&$.error("SQL operation unknown for operator "+k.operator),m.accept_values&&(k.value instanceof Array?!l.list&&k.value.length>1&&$.error("Operator "+k.operator+" cannot accept multiple values"):k.value=[k.value],k.value.forEach(function(b,c){c>0&&(n+=l.sep),"integer"==k.type||"double"==k.type?b=d(b,k.type):a||(b=g(b)),l.fn&&(b=l.fn(b)),a?(n+="question_mark"==a?"?":"$"+f,h.push(b),f++):("string"==typeof b&&(b="'"+b+"'"),n+=b)})),i.push(k.field+" "+l.op.replace(/\?/,n))}}),i.join(" "+c.condition+b)}(c);return a?{sql:i,params:h}:{sql:i}},getSqlOperator:function(a){var b=this.settings.sqlOperators[a];return void 0===b?!1:("string"==typeof b&&(b={op:b}),b.list||(b.list=!1),b.list&&!b.sep&&(b.sep=", "),b)}})}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.js deleted file mode 100644 index 0e32f20bd..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.js +++ /dev/null @@ -1,2623 +0,0 @@ -/*! - * MicroEvent - to make any js object an event emitter - * Copyright 2011 Jerome Etienne (http://jetienne.com) - * Copyright 2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) - * Licensed under MIT (http://opensource.org/licenses/MIT) - */ - -(function(root, factory) { - if (typeof module !== 'undefined' && module.exports) { - module.exports = factory(); - } - else if (typeof define === 'function' && define.amd) { - define('microevent', [], factory); - } - else { - root.MicroEvent = factory(); - } -}(this, function() { - "use strict"; - - var MicroEvent = function(){}; - - MicroEvent.prototype = { - /** - * Add one or many event handlers - * - * @param {String,Object} events - * @param {Function} optional, callback - * - * obj.on('event', callback) - * obj.on('event1 event2', callback) - * obj.on({ event1: callback1, event2: callback2 }) - */ - on: function (events, fct) { - this._events = this._events || {}; - - if (typeof events === 'object') { - for (var event in events) { - if (events.hasOwnProperty(event)) { - this._events[event] = this._events[event] || []; - this._events[event].push(events[event]); - } - } - } - else { - events.split(' ').forEach(function(event) { - this._events[event] = this._events[event] || []; - this._events[event].push(fct); - }, this); - } - - return this; - }, - - /** - * Remove one or many or all event handlers - * - * @param {String,Object} optional, events - * @param {Function} optional, callback - * - * obj.off('event') - * obj.off('event', callback) - * obj.off('event1 event2') - * obj.off({ event1: callback1, event2: callback2 }) - * obj.off() - */ - off: function (events, fct) { - this._events = this._events || {}; - - if (typeof events === 'object') { - for (var event in events) { - if (events.hasOwnProperty(event) && (event in this._events)) { - var index = this._events[event].indexOf(events[event]); - if (index !== -1) this._events[event].splice(index, 1); - } - } - } - else if (!!events) { - events.split(' ').forEach(function(event) { - if (event in this._events) { - if (fct) { - var index = this._events[event].indexOf(fct); - if (index !== -1) this._events[event].splice(index, 1); - } - else { - this._events[event] = []; - } - } - }, this); - } - else { - this._events = {}; - } - - return this; - }, - - /** - * Add one or many event handlers that will be called only once - * This handlers are only applicable to "trigger", not "change" - * - * @param {String,Object} events - * @param {Function} optional, callback - * - * obj.once('event', callback) - * obj.once('event1 event2', callback) - * obj.once({ event1: callback1, event2: callback2 }) - */ - once: function (events, fct) { - this._once = this._once || {}; - - if (typeof events === 'object') { - for (var event in events) { - if (events.hasOwnProperty(event)) { - this._once[event] = this._once[event] || []; - this._once[event].push(events[event]); - } - } - } - else { - events.split(' ').forEach(function(event) { - this._once[event] = this._once[event] || []; - this._once[event].push(fct); - }, this); - } - - return this; - }, - - /** - * Trigger all handlers for an event - * - * @param {String} event name - * @param {Mixed...} optional, arguments - */ - trigger: function (event /* , args... */) { - this._events = this._events || {}; - this._once = this._once || {}; - - var args = Array.prototype.slice.call(arguments, 1), - callbacks; - - if (event in this._events) { - callbacks = this._events[event].slice(); - while (callbacks.length) { - callbacks.shift().apply(this, args); - } - } - - if (event in this._once) { - callbacks = this._once[event].slice(); - while (callbacks.length) { - callbacks.shift().apply(this, args); - } - delete this._once[event]; - } - - return this; - }, - - /** - * Trigger all modificators for an event, each handler must return a value - * - * @param {String} event name - * @param {Mixed} event value - * @param {Mixed...} optional, arguments - */ - change: function(event, value /* , args... */) { - this._events = this._events || {}; - - if (event in this._events) { - var args = Array.prototype.slice.call(arguments, 1); - - for (var i=0, l=this._events[event].length; i.rules-group-container>.rules-group-body>.rules-list').empty(); - - this.addRule(this.$el.find('>.rules-group-container')); - - this.trigger('afterReset'); - }; - - /** - * Clear the plugin - */ - QueryBuilder.prototype.clear = function() { - this.status.group_id = 0; - this.status.rule_id = 0; - - this.$el.empty(); - - this.trigger('afterClear'); - }; - - /** - * Get an object representing current rules - * @return {object} - */ - QueryBuilder.prototype.getRules = function() { - this.clearErrors(); - - var $group = this.$el.find('>.rules-group-container'), - that = this; - - var rules = (function parse($group) { - var out = {}, - $elements = $group.find('>.rules-group-body>.rules-list>*'); - - out.condition = that.getGroupCondition($group); - out.rules = []; - - for (var i=0, l=$elements.length; i 1)) { - that.triggerValidationError(['empty_group'], $group, null, null, null); - return {}; - } - - return out; - }($group)); - - return this.change('getRules', rules); - }; - - /** - * Set rules from object - * @param data {object} - */ - QueryBuilder.prototype.setRules = function(data) { - this.clear(); - - if (!data || !data.rules || (data.rules.length===0 && !this.settings.allow_empty)) { - $.error('Incorrect data object passed'); - } - - data = this.change('setRules', data); - - var $container = this.$el, - that = this; - - (function add(data, $container){ - var $group = that.addGroup($container, false); - if ($group === null) { - return; - } - - var $buttons = $group.find('>.rules-group-header [name$=_cond]'); - - if (data.condition === undefined) { - data.condition = that.settings.default_condition; - } - - for (var i=0, l=that.settings.conditions.length; i0) { - if (that.settings.allow_groups !== -1 && that.settings.allow_groups < $group.data('queryBuilder').level) { - that.reset(); - $.error(fmt('No more than {0} groups are allowed', that.settings.allow_groups)); - } - else { - add(rule, $group); - } - } - else { - if (rule.id === undefined) { - $.error('Missing rule field id'); - } - if (rule.value === undefined) { - rule.value = ''; - } - if (rule.operator === undefined) { - rule.operator = 'equal'; - } - - var $rule = that.addRule($group); - if ($rule === null) { - return; - } - - var filter = that.getFilterById(rule.id), - operator = that.getOperatorByType(rule.operator); - - $rule.find('.rule-filter-container [name$=_filter]').val(rule.id).trigger('change'); - $rule.find('.rule-operator-container [name$=_operator]').val(rule.operator).trigger('change'); - - if (operator.accept_values !== 0) { - that.setRuleValue($rule, rule.value, filter, operator); - } - - that.applyRuleFlags($rule, rule); - } - }); - - }(data, $container)); - }; - - - /** - * Checks the configuration of each filter - */ - QueryBuilder.prototype.checkFilters = function() { - var definedFilters = [], - that = this; - - $.each(this.filters, function(i, filter) { - if (!filter.id) { - $.error('Missing filter id: '+ i); - } - if (definedFilters.indexOf(filter.id) != -1) { - $.error('Filter already defined: '+ filter.id); - } - definedFilters.push(filter.id); - - if (!filter.type) { - $.error('Missing filter type: '+ filter.id); - } - if (types.indexOf(filter.type) == -1) { - $.error('Invalid type: '+ filter.type); - } - - if (!filter.input) { - filter.input = 'text'; - } - else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) { - $.error('Invalid input: '+ filter.input); - } - - if (!filter.field) { - filter.field = filter.id; - } - if (!filter.label) { - filter.label = filter.field; - } - - that.status.has_optgroup|= !!filter.optgroup; - if (!filter.optgroup) { - filter.optgroup = null; - } - - switch (filter.type) { - case 'string': - filter.internalType = 'string'; - break; - case 'integer': case 'double': - filter.internalType = 'number'; - break; - case 'date': case 'time': case 'datetime': - filter.internalType = 'datetime'; - break; - } - - switch (filter.input) { - case 'radio': case 'checkbox': - if (!filter.values || filter.values.length < 1) { - $.error('Missing values for filter: '+ filter.id); - } - break; - } - }); - - // group filters with same optgroup, preserving declaration order when possible - if (this.status.has_optgroup) { - var optgroups = [], - filters = []; - - $.each(this.filters, function(i, filter) { - var idx; - - if (filter.optgroup) { - idx = optgroups.lastIndexOf(filter.optgroup); - - if (idx == -1) { - idx = optgroups.length; - } - } - else { - idx = optgroups.length; - } - - optgroups.splice(idx, 0, filter.optgroup); - filters.splice(idx, 0, filter); - }); - - this.filters = filters; - } - - this.trigger('afterCheckFilters'); - }; - - /** - * Add all events listeners - */ - QueryBuilder.prototype.bindEvents = function() { - var that = this; - - // group condition change - this.$el.on('change.queryBuilder', '.rules-group-header [name$=_cond]', function() { - var $this = $(this); - - if ($this.is(':checked')) { - $this.parent().addClass('active').siblings().removeClass('active'); - } - }); - - // rule filter change - this.$el.on('change.queryBuilder', '.rule-filter-container [name$=_filter]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleFilter($rule, $this.val()); - }); - - // rule operator change - this.$el.on('change.queryBuilder', '.rule-operator-container [name$=_operator]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.updateRuleOperator($rule, $this.val()); - }); - - // add rule button - this.$el.on('click.queryBuilder', '[data-add=rule]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addRule($group); - }); - - // delete rule button - this.$el.on('click.queryBuilder', '[data-delete=rule]', function() { - var $this = $(this), - $rule = $this.closest('.rule-container'); - - that.deleteRule($rule); - }); - - if (this.settings.allow_groups !== 0) { - // add group button - this.$el.on('click.queryBuilder', '[data-add=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.addGroup($group); - }); - - // delete group button - this.$el.on('click.queryBuilder', '[data-delete=group]', function() { - var $this = $(this), - $group = $this.closest('.rules-group-container'); - - that.deleteGroup($group); - }); - } - }; - - /** - * Add a new rules group - * @param $parent {jQuery} - * @param addRule {bool} (optional - add a default empty rule) - * @return $group {jQuery} - */ - QueryBuilder.prototype.addGroup = function($parent, addRule) { - var group_id = this.nextGroupId(), - level = (($parent.data('queryBuilder') || {}).level || 0) + 1, - $container = level===1 ? $parent : $parent.find('>.rules-group-body>.rules-list'), - $group = $(this.template.group.call(this, group_id, level)); - - $group.data('queryBuilder', {level:level}); - - var e = $.Event('addGroup.queryBuilder', { - group_id: group_id, - level: level, - addRule: addRule, - group: $group, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($group); - - if (this.settings.onAfterAddGroup) { - this.settings.onAfterAddGroup.call(this, $group); - } - - this.trigger('afterAddGroup', $group); - - if (addRule === undefined || addRule === true) { - this.addRule($group); - } - - return $group; - }; - - /** - * Tries to delete a group. The group is not deleted if at least one rule is no_delete. - * @param $group {jQuery} - * @return {boolean} true if the group has been deleted - */ - QueryBuilder.prototype.deleteGroup = function($group) { - if ($group[0].id == this.$el_id + '_group_0') { - return; - } - - var e = $.Event('deleteGroup.queryBuilder', { - group_id: $group[0].id, - group: $group, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteGroup', $group); - - var that = this, - keepGroup = false; - - $group.find('>.rules-group-body>.rules-list>*').each(function() { - var $element = $(this); - - if ($element.hasClass('rule-container')) { - if ($element.data('queryBuilder').flags.no_delete) { - keepGroup = true; - } - else { - $element.remove(); - } - } - else { - keepGroup|= !that.deleteGroup($element); - } - }); - - if (!keepGroup) { - $group.remove(); - } - - return !keepGroup; - }; - - /** - * Add a new rule - * @param $parent {jQuery} - * @return $rule {jQuery} - */ - QueryBuilder.prototype.addRule = function($parent) { - var rule_id = this.nextRuleId(), - $container = $parent.find('>.rules-group-body>.rules-list'), - $rule = $(this.template.rule.call(this, rule_id)), - $filterSelect = $(this.getRuleFilterSelect(rule_id)); - - $rule.data('queryBuilder', {flags: {}}); - - var e = $.Event('addRule.queryBuilder', { - rule_id: rule_id, - rule: $rule, - parent: $parent, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return null; - } - - $container.append($rule); - $rule.find('.rule-filter-container').append($filterSelect); - - if (this.settings.onAfterAddRule) { - this.settings.onAfterAddRule.call(this, $rule); - } - - this.trigger('afterAddRule', $rule); - - return $rule; - }; - - /** - * Delete a rule. - * @param $rule {jQuery} - * @return {boolean} true if the rule has been deleted - */ - QueryBuilder.prototype.deleteRule = function($rule) { - var e = $.Event('deleteRule.queryBuilder', { - rule_id: $rule[0].id, - rule: $rule, - builder: this - }); - - this.$el.trigger(e); - - if (e.isDefaultPrevented()) { - return false; - } - - this.trigger('beforeDeleteRule', $rule); - - $rule.remove(); - return true; - }; - - /** - * Create operators for a rule - * @param $rule {jQuery} (
    • element) - * @param filter {object} - */ - QueryBuilder.prototype.createRuleInput = function($rule, filter) { - var $valueContainer = $rule.find('.rule-value-container').empty(); - - if (filter === null) { - return; - } - - var operator = this.getOperatorByType(this.getRuleOperator($rule)); - - if (operator.accept_values === 0) { - return; - } - - var $inputs = $(); - - for (var i=0; i 0) $valueContainer.append(' , '); - $valueContainer.append($ruleInput); - $inputs = $inputs.add($ruleInput); - } - - $valueContainer.show(); - - if (filter.onAfterCreateRuleInput) { - filter.onAfterCreateRuleInput.call(this, $rule, filter); - } - - if (filter.plugin) { - $inputs[filter.plugin](filter.plugin_config || {}); - } - - if (filter.default_value !== undefined) { - this.setRuleValue($rule, filter.default_value, filter, operator); - } - - this.trigger('afterCreateRuleInput', $rule, filter, operator); - }; - - /** - * Perform action when rule's filter is changed - * @param $rule {jQuery} (
    • element) - * @param filterId {string} - */ - QueryBuilder.prototype.updateRuleFilter = function($rule, filterId) { - var filter = filterId != '-1' ? this.getFilterById(filterId) : null; - - this.createRuleOperators($rule, filter); - this.createRuleInput($rule, filter); - - $rule.data('queryBuilder').filter = filter; - - this.trigger('afterUpdateRuleFilter', $rule, filter); - }; - - /** - * Update main visibility when rule operator changes - * @param $rule {jQuery} (
    • element) - * @param operatorType {string} - */ - QueryBuilder.prototype.updateRuleOperator = function($rule, operatorType) { - var $valueContainer = $rule.find('.rule-value-container'), - filter = this.getFilterById(this.getRuleFilter($rule)), - operator = this.getOperatorByType(operatorType); - - if (operator.accept_values === 0) { - $valueContainer.hide(); - } - else { - $valueContainer.show(); - - var previousOperator = $rule.data('queryBuilder').operator; - - if ($valueContainer.is(':empty') || operator.accept_values != previousOperator.accept_values) { - this.createRuleInput($rule, filter); - } - } - - $rule.data('queryBuilder').operator = operator; - - if (filter.onAfterChangeOperator) { - filter.onAfterChangeOperator.call(this, $rule, filter, operator); - } - - this.trigger('afterChangeOperator', $rule, filter, operator); - }; - - /** - * Check if a value is correct for a filter - * @param $rule {jQuery} (
    • element) - * @param value {string|string[]|undefined} - * @param filter {object} - * @param operator {object} - * @return {array|true} - */ - QueryBuilder.prototype.validateValue = function($rule, value, filter, operator) { - var validation = filter.validation || {}, - result = true; - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - if (validation.callback) { - result = validation.callback.call(this, value, filter, operator, $rule); - return this.change('validateValue', result, $rule, value, filter, operator); - } - - for (var i=0; i validation.max) { - result = ['string_exceed_max_length', validation.max]; - break; - } - } - if (validation.format) { - if (!(validation.format.test(value[i]))) { - result = ['string_invalid_format', validation.format]; - break; - } - } - break; - - case 'number': - if (isNaN(value[i])) { - result = ['number_nan']; - break; - } - if (filter.type == 'integer') { - if (parseInt(value[i]) != value[i]) { - result = ['number_not_integer']; - break; - } - } - else { - if (parseFloat(value[i]) != value[i]) { - result = ['number_not_double']; - break; - } - } - if (validation.min !== undefined) { - if (value[i] < validation.min) { - result = ['number_exceed_min', validation.min]; - break; - } - } - if (validation.max !== undefined) { - if (value[i] > validation.max) { - result = ['number_exceed_max', validation.max]; - break; - } - } - if (validation.step !== undefined) { - var v = value[i]/validation.step; - if (parseInt(v) != v) { - result = ['number_wrong_step', validation.step]; - break; - } - } - break; - - case 'datetime': - // we need MomentJS - if (window.moment && validation.format) { - var datetime = moment(value[i], validation.format); - if (!datetime.isValid()) { - result = ['datetime_invalid']; - break; - } - else { - if (validation.min) { - if (datetime < moment(validation.min, validation.format)) { - result = ['datetime_exceed_min', validation.min]; - break; - } - } - if (validation.max) { - if (datetime > moment(validation.max, validation.format)) { - result = ['datetime_exceed_max', validation.max]; - break; - } - } - } - } - break; - } - } - - if (result !== true) { - break; - } - } - - return this.change('validateValue', result, $rule, value, filter, operator); - }; - - /** - * Remove 'has-error' from everything - */ - QueryBuilder.prototype.clearErrors = function() { - this.$el.find('.has-error').removeClass('has-error'); - }; - - /** - * Trigger a validation error event with custom params - * @param error {array} - * @param $target {jQuery} - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.triggerValidationError = function(error, $target, value, filter, operator) { - if (!$.isArray(error)) { - error = [error]; - } - - if (filter && filter.onValidationError) { - filter.onValidationError.call(this, $target, error, value, filter, operator); - } - if (this.settings.onValidationError) { - this.settings.onValidationError.call(this, $target, error, value, filter, operator); - } - - var e = $.Event('validationError.queryBuilder', { - error: error, - filter: filter, - operator: operator, - value: value, - targetRule: $target[0], - builder: this - }); - - this.$el.trigger(e); - - if (this.settings.display_errors && !e.isDefaultPrevented()) { - // translate the text without modifying event array - var errorLoc = $.extend([], error, [ - this.lang.errors[error[0]] || error[0] - ]); - - $target.addClass('has-error'); - var $error = $target.find('.error-container').eq(0); - $error.attr('title', fmt.apply(null, errorLoc)); - } - - this.trigger('validationError', $target, error); - }; - - - /** - * Returns an incremented group ID - * @return {string} - */ - QueryBuilder.prototype.nextGroupId = function() { - return this.$el_id + '_group_' + (this.status.group_id++); - }; - - /** - * Returns an incremented rule ID - * @return {string} - */ - QueryBuilder.prototype.nextRuleId = function() { - return this.$el_id + '_rule_' + (this.status.rule_id++); - }; - - /** - * Returns the operators for a filter - * @param filter {string|object} (filter id name or filter object) - * @return {object[]} - */ - QueryBuilder.prototype.getOperators = function(filter) { - if (typeof filter === 'string') { - filter = this.getFilterById(filter); - } - - var result = []; - - for (var i=0, l=this.operators.length; i element) - * @return {string} - */ - QueryBuilder.prototype.getGroupCondition = function($group) { - return $group.find('>.rules-group-header [name$=_cond]:checked').val(); - }; - - /** - * Returns the selected filter of a rule - * @param $rule {jQuery} (
    • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleFilter = function($rule) { - return $rule.find('.rule-filter-container [name$=_filter]').val(); - }; - - /** - * Returns the selected operator of a rule - * @param $rule {jQuery} (
    • element) - * @return {string} - */ - QueryBuilder.prototype.getRuleOperator = function($rule) { - return $rule.find('.rule-operator-container [name$=_operator]').val(); - }; - - /** - * Returns rule value - * @param $rule {jQuery} (
    • element) - * @param filter {object} (optional - current rule filter) - * @param operator {object} (optional - current rule operator) - * @return {string|string[]|undefined} - */ - QueryBuilder.prototype.getRuleValue = function($rule, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - var value = [], tmp, - $value = $rule.find('.rule-value-container'); - - for (var i=0; i element) - * @param value {mixed} - * @param filter {object} - * @param operator {object} - */ - QueryBuilder.prototype.setRuleValue = function($rule, value, filter, operator) { - filter = filter || this.getFilterById(this.getRuleFilter($rule)); - operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); - - this.trigger('beforeSetRuleValue', $rule, value, filter, operator); - - if (filter.valueSetter) { - filter.valueSetter.call(this, $rule, value, filter, operator); - } - else { - var $value = $rule.find('.rule-value-container'); - - if (operator.accept_values == 1) { - value = [value]; - } - else { - value = value; - } - - for (var i=0; i element) - * @param rule {object} - */ - QueryBuilder.prototype.applyRuleFlags = function($rule, rule) { - var flags = this.getRuleFlags(rule); - $rule.data('queryBuilder').flags = flags; - - if (flags.filter_readonly) { - $rule.find('[name$=_filter]').prop('disabled', true); - } - if (flags.operator_readonly) { - $rule.find('[name$=_operator]').prop('disabled', true); - } - if (flags.value_readonly) { - $rule.find('[name*=_value_]').prop('disabled', true); - } - if (flags.no_delete) { - $rule.find('[data-delete=rule]').remove(); - } - - this.trigger('afterApplyRuleFlags', $rule, rule, flags); - }; - - - /** - * Returns group HTML - * @param group_id {string} - * @param level {int} - * @return {string} - */ - QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { - var h = '\ -
      \ -
      \ -
      \ - \ - '+ (this.settings.allow_groups===-1 || this.settings.allow_groups>=level ? - '' - :'') +' \ - '+ (level>1 ? - '' - : '') +' \ -
      \ -
      \ - '+ this.getGroupConditions(group_id) +' \ -
      \ - '+ (this.settings.display_errors ? - '
      ' - :'') +'\ -
      \ -
      \ -
        \ -
        \ -
        '; - - return this.change('getGroupTemplate', h, level); - }; - - /** - * Returns group conditions HTML - * @param group_id {string} - * @return {string} - */ - QueryBuilder.prototype.getGroupConditions = function(group_id) { - var h = ''; - - for (var i=0, l=this.settings.conditions.length; i \ - '+ label +' \ - '; - } - - return this.change('getGroupConditions', h); - }; - - /** - * Returns rule HTML - * @param rule_id {string} - * @return {string} - */ - QueryBuilder.prototype.getRuleTemplate = function(rule_id) { - var h = '\ -
      • \ -
        \ -
        \ - \ -
        \ -
        \ - '+ (this.settings.display_errors ? - '
        ' - :'') +'\ -
        \ -
        \ -
        \ -
      • '; - - return this.change('getRuleTemplate', h); - }; - - /** - * Returns rule filter '; - h+= ''; - - $.each(this.filters, function(i, filter) { - if (optgroup != filter.optgroup) { - if (optgroup !== null) h+= ''; - optgroup = filter.optgroup; - if (optgroup !== null) h+= ''; - } - - h+= ''; - }); - - if (optgroup !== null) h+= ''; - h+= ''; - - return this.change('getRuleFilterSelect', h); - }; - - /** - * Returns rule operator '; - - for (var i=0, l=operators.length; i'+ label +''; - } - - h+= ''; - - return this.change('getRuleOperatorSelect', h); - }; - - /** - * Return the rule value HTML - * @param $rule {jQuery} - * @param filter {object} - * @param value_id {int} - * @return {string} - */ - QueryBuilder.prototype.getRuleInput = function($rule, filter, value_id) { - var validation = filter.validation || {}, - name = $rule[0].id +'_value_'+ value_id, - h = '', c; - - if (typeof filter.input === 'function') { - h = filter.input.call(this, $rule, filter, name); - } - else { - switch (filter.input) { - case 'radio': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'checkbox': - c = filter.vertical ? ' class=block' : ''; - iterateOptions(filter.values, function(key, val) { - h+= ' '+ val +' '; - }); - break; - - case 'select': - h+= ''; - break; - - case 'textarea': - h+= '";break;default:switch(c.internalType){case"number":h+='1&&$.error("Unable to initialize on multiple target");var b=this.data("queryBuilder"),c="object"==typeof a&&a||{};return b||"destroy"!=a?(b||this.data("queryBuilder",new j(this,c)),"string"==typeof a?b[a].apply(b,Array.prototype.slice.call(arguments,1)):this):this},$.fn.queryBuilder.defaults={set:function(a){$.extendext(!0,"replace",j.DEFAULTS,a)},get:function(a){var b=j.DEFAULTS;return a&&(b=b[a]),$.extend(!0,{},b)}},$.fn.queryBuilder.constructor=j,$.fn.queryBuilder.extend=j.extend,$.fn.queryBuilder.define=j.define,$.fn.queryBuilder.define("bt-selectpicker",function(a){$.fn.selectpicker&&$.fn.selectpicker.Constructor||$.error('Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'),a=$.extend({container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1},a||{}),this.on("afterAddRule",function(b){b.find(".rule-filter-container select").selectpicker(a)}),this.on("afterCreateRuleOperators",function(b){b.find(".rule-operator-container select").selectpicker(a)})}),$.fn.queryBuilder.define("bt-tooltip-errors",function(a){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||$.error('Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'),a=$.extend({placement:"right"},a||{}),this.on("getRuleTemplate",function(a){return a.replace('class="error-container"','class="error-container" data-toggle="tooltip"')}),this.on("validationError",function(b){b.find(".error-container").eq(0).tooltip(a).tooltip("hide").tooltip("fixTitle")})}),$.fn.queryBuilder.define("filter-description",function(a){a=$.extend({icon:"glyphicon glyphicon-info-sign",mode:"popover"},a||{}),"inline"===a.mode?this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("p.filter-description");c&&c.description?(0===d.length?(d=$('

        '),d.appendTo(b)):d.show(),d.html(' '+c.description)):d.hide()}):"popover"===a.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||$.error('Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length?(d=$(''),d.prependTo(b.find(".rule-actions")),d.popover({placement:"left",container:"body",html:!0}),d.on("mouseout",function(){d.popover("hide")})):d.show(),d.data("bs.popover").options.content=c.description,d.attr("aria-describedby")&&d.popover("show")):(d.hide(),d.data("bs.popover")&&d.popover("hide"))})):"bootbox"===a.mode&&(window.bootbox||$.error('Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length&&(d=$(''),d.prependTo(b.find(".rule-actions")),d.on("click",function(){bootbox.alert(d.data("description"))})),d.data("description",c.description)):d.hide()}))}),$.fn.queryBuilder.defaults.set({mongoOperators:{equal:function(a){return a[0]},not_equal:function(a){return{$ne:a[0]}},"in":function(a){return{$in:a}},not_in:function(a){return{$nin:a}},less:function(a){return{$lt:a[0]}},less_or_equal:function(a){return{$lte:a[0]}},greater:function(a){return{$gt:a[0]}},greater_or_equal:function(a){return{$gte:a[0]}},between:function(a){return{$gte:a[0],$lte:a[1]}},begins_with:function(a){return{$regex:"^"+e(a[0])}},not_begins_with:function(a){return{$regex:"^(?!"+e(a[0])+")"}},contains:function(a){return{$regex:e(a[0])}},not_contains:function(a){return{$regex:"^((?!"+e(a[0])+").)*$",$options:"s"}},ends_with:function(a){return{$regex:e(a[0])+"$"}},not_ends_with:function(a){return{$regex:"(?0)e.push(c(f));else{var g=b.settings.mongoOperators[f.operator],h=b.getOperatorByType(f.operator),i=[];void 0===g&&$.error("MongoDB operation unknown for operator "+f.operator),h.accept_values&&(f.value instanceof Array||(f.value=[f.value]),f.value.forEach(function(a){i.push(d(a,f.type))}));var j={};j[f.field]=g.call(b,i),e.push(j)}});var f={};return e.length>0&&(f["$"+a.condition.toLowerCase()]=e),f}(a)}}),$.fn.queryBuilder.define("sortable",function(a){a=$.extend({default_no_sortable:!1,icon:"glyphicon glyphicon-sort"},a||{}),this.on("afterInit",function(){$.event.props.push("dataTransfer");var a,b,c=this;this.$el.on("mouseover",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").attr("draggable",!0)}),this.$el.on("mouseout",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")}),this.$el.on("dragstart","[draggable]",function(c){c.stopPropagation(),c.dataTransfer.setData("text","drag"),b=$(c.target),a=$('
         
        '),a.css("min-height",b.height()),a.insertAfter(b),setTimeout(function(){b.hide()},0)}),this.$el.on("dragenter","[draggable]",function(b){b.preventDefault(),b.stopPropagation(),f(a,$(b.target))}),this.$el.on("dragover","[draggable]",function(a){a.preventDefault(),a.stopPropagation()}),this.$el.on("drop",function(a){a.preventDefault(),a.stopPropagation(),f(b,$(a.target))}),this.$el.on("dragend","[draggable]",function(d){d.preventDefault(),d.stopPropagation(),b.show(),a.remove(),b=a=null,c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")})}),this.on("getRuleFlags",function(b){return void 0===b.no_sortable&&(b.no_sortable=a.default_no_sortable),b}),this.on("afterApplyRuleFlags",function(a,b,c){c.no_sortable&&a.find(".drag-handle").remove()}),this.on("getGroupTemplate",function(b,c){if(c>1){var d=$(b);d.find(".group-conditions").after('
        '),b=d.prop("outerHTML")}return b}),this.on("getRuleTemplate",function(b){var c=$(b);return c.find(".rule-header").after('
        '),c.prop("outerHTML")})}),$.fn.queryBuilder.defaults.set({sqlOperators:{equal:"= ?",not_equal:"!= ?","in":{op:"IN(?)",list:!0,sep:", "},not_in:{op:"NOT IN(?)",list:!0,sep:", "},less:"< ?",less_or_equal:"<= ?",greater:"> ?",greater_or_equal:">= ?",between:{op:"BETWEEN ?",list:!0,sep:" AND "},begins_with:{op:"LIKE(?)",fn:function(a){return a+"%"}},not_begins_with:{op:"NOT LIKE(?)",fn:function(a){return a+"%"}},contains:{op:"LIKE(?)",fn:function(a){return"%"+a+"%"}},not_contains:{op:"NOT LIKE(?)",fn:function(a){return"%"+a+"%"}},ends_with:{op:"LIKE(?)",fn:function(a){return"%"+a}},not_ends_with:{op:"NOT LIKE(?)",fn:function(a){return"%"+a}},is_empty:'== ""',is_not_empty:'!= ""',is_null:"IS NULL",is_not_null:"IS NOT NULL"}}),$.fn.queryBuilder.extend({getSQL:function(a,b,c){c=void 0===c?this.getRules():c,a=a===!0||void 0===a?"question_mark":a,b=b||void 0===b?"\n":" ";var e=this,f=1,h=[],i=function j(c){if(c.condition||(c.condition=e.settings.default_condition),-1===["AND","OR"].indexOf(c.condition.toUpperCase())&&$.error("Unable to build SQL query with "+c.condition+" condition"),!c.rules)return"";var i=[];return $.each(c.rules,function(c,k){if(k.rules&&k.rules.length>0)i.push("("+b+j(k)+b+")"+b);else{var l=e.getSqlOperator(k.operator),m=e.getOperatorByType(k.operator),n="";l===!1&&$.error("SQL operation unknown for operator "+k.operator),m.accept_values&&(k.value instanceof Array?!l.list&&k.value.length>1&&$.error("Operator "+k.operator+" cannot accept multiple values"):k.value=[k.value],k.value.forEach(function(b,c){c>0&&(n+=l.sep),"integer"==k.type||"double"==k.type?b=d(b,k.type):a||(b=g(b)),l.fn&&(b=l.fn(b)),a?(n+="question_mark"==a?"?":"$"+f,h.push(b),f++):("string"==typeof b&&(b="'"+b+"'"),n+=b)})),i.push(k.field+" "+l.op.replace(/\?/,n))}}),i.join(" "+c.condition+b)}(c);return a?{sql:i,params:h}:{sql:i}},getSqlOperator:function(a){var b=this.settings.sqlOperators[a];return void 0===b?!1:("string"==typeof b&&(b={op:b}),b.list||(b.list=!1),b.list&&!b.sep&&(b.sep=", "),b)}})}); \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery-ui-1.10.3.custom.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery-ui-1.10.3.custom.js deleted file mode 100755 index 8946b4247..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery-ui-1.10.3.custom.js +++ /dev/null @@ -1,2252 +0,0 @@ -/*! jQuery UI - v1.10.3 - 2013-09-02 -* http://jqueryui.com -* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.sortable.js -* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ - -(function( $, undefined ) { - -var uuid = 0, - runiqueId = /^ui-id-\d+$/; - -// $.ui might exist from components with no dependencies, e.g., $.ui.position -$.ui = $.ui || {}; - -$.extend( $.ui, { - version: "1.10.3", - - keyCode: { - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - LEFT: 37, - NUMPAD_ADD: 107, - NUMPAD_DECIMAL: 110, - NUMPAD_DIVIDE: 111, - NUMPAD_ENTER: 108, - NUMPAD_MULTIPLY: 106, - NUMPAD_SUBTRACT: 109, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - RIGHT: 39, - SPACE: 32, - TAB: 9, - UP: 38 - } -}); - -// plugins -$.fn.extend({ - focus: (function( orig ) { - return function( delay, fn ) { - return typeof delay === "number" ? - this.each(function() { - var elem = this; - setTimeout(function() { - $( elem ).focus(); - if ( fn ) { - fn.call( elem ); - } - }, delay ); - }) : - orig.apply( this, arguments ); - }; - })( $.fn.focus ), - - scrollParent: function() { - var scrollParent; - if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { - scrollParent = this.parents().filter(function() { - return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } else { - scrollParent = this.parents().filter(function() { - return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } - - return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; - }, - - zIndex: function( zIndex ) { - if ( zIndex !== undefined ) { - return this.css( "zIndex", zIndex ); - } - - if ( this.length ) { - var elem = $( this[ 0 ] ), position, value; - while ( elem.length && elem[ 0 ] !== document ) { - // Ignore z-index if position is set to a value where z-index is ignored by the browser - // This makes behavior of this function consistent across browsers - // WebKit always returns auto if the element is positioned - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { - // IE returns 0 when zIndex is not specified - // other browsers return a string - // we ignore the case of nested elements with an explicit value of 0 - //
        - value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; - } - } - elem = elem.parent(); - } - } - - return 0; - }, - - uniqueId: function() { - return this.each(function() { - if ( !this.id ) { - this.id = "ui-id-" + (++uuid); - } - }); - }, - - removeUniqueId: function() { - return this.each(function() { - if ( runiqueId.test( this.id ) ) { - $( this ).removeAttr( "id" ); - } - }); - } -}); - -// selectors -function focusable( element, isTabIndexNotNaN ) { - var map, mapName, img, - nodeName = element.nodeName.toLowerCase(); - if ( "area" === nodeName ) { - map = element.parentNode; - mapName = map.name; - if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { - return false; - } - img = $( "img[usemap=#" + mapName + "]" )[0]; - return !!img && visible( img ); - } - return ( /input|select|textarea|button|object/.test( nodeName ) ? - !element.disabled : - "a" === nodeName ? - element.href || isTabIndexNotNaN : - isTabIndexNotNaN) && - // the element and all of its ancestors must be visible - visible( element ); -} - -function visible( element ) { - return $.expr.filters.visible( element ) && - !$( element ).parents().addBack().filter(function() { - return $.css( this, "visibility" ) === "hidden"; - }).length; -} - -$.extend( $.expr[ ":" ], { - data: $.expr.createPseudo ? - $.expr.createPseudo(function( dataName ) { - return function( elem ) { - return !!$.data( elem, dataName ); - }; - }) : - // support: jQuery <1.8 - function( elem, i, match ) { - return !!$.data( elem, match[ 3 ] ); - }, - - focusable: function( element ) { - return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); - }, - - tabbable: function( element ) { - var tabIndex = $.attr( element, "tabindex" ), - isTabIndexNaN = isNaN( tabIndex ); - return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); - } -}); - -// support: jQuery <1.8 -if ( !$( "" ).outerWidth( 1 ).jquery ) { - $.each( [ "Width", "Height" ], function( i, name ) { - var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], - type = name.toLowerCase(), - orig = { - innerWidth: $.fn.innerWidth, - innerHeight: $.fn.innerHeight, - outerWidth: $.fn.outerWidth, - outerHeight: $.fn.outerHeight - }; - - function reduce( elem, size, border, margin ) { - $.each( side, function() { - size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; - if ( border ) { - size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; - } - if ( margin ) { - size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; - } - }); - return size; - } - - $.fn[ "inner" + name ] = function( size ) { - if ( size === undefined ) { - return orig[ "inner" + name ].call( this ); - } - - return this.each(function() { - $( this ).css( type, reduce( this, size ) + "px" ); - }); - }; - - $.fn[ "outer" + name] = function( size, margin ) { - if ( typeof size !== "number" ) { - return orig[ "outer" + name ].call( this, size ); - } - - return this.each(function() { - $( this).css( type, reduce( this, size, true, margin ) + "px" ); - }); - }; - }); -} - -// support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - -// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) -if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { - $.fn.removeData = (function( removeData ) { - return function( key ) { - if ( arguments.length ) { - return removeData.call( this, $.camelCase( key ) ); - } else { - return removeData.call( this ); - } - }; - })( $.fn.removeData ); -} - - - - - -// deprecated -$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - -$.support.selectstart = "onselectstart" in document.createElement( "div" ); -$.fn.extend({ - disableSelection: function() { - return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + - ".ui-disableSelection", function( event ) { - event.preventDefault(); - }); - }, - - enableSelection: function() { - return this.unbind( ".ui-disableSelection" ); - } -}); - -$.extend( $.ui, { - // $.ui.plugin is deprecated. Use $.widget() extensions instead. - plugin: { - add: function( module, option, set ) { - var i, - proto = $.ui[ module ].prototype; - for ( i in set ) { - proto.plugins[ i ] = proto.plugins[ i ] || []; - proto.plugins[ i ].push( [ option, set[ i ] ] ); - } - }, - call: function( instance, name, args ) { - var i, - set = instance.plugins[ name ]; - if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { - return; - } - - for ( i = 0; i < set.length; i++ ) { - if ( instance.options[ set[ i ][ 0 ] ] ) { - set[ i ][ 1 ].apply( instance.element, args ); - } - } - } - }, - - // only used by resizable - hasScroll: function( el, a ) { - - //If overflow is hidden, the element might have extra content, but the user wants to hide it - if ( $( el ).css( "overflow" ) === "hidden") { - return false; - } - - var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", - has = false; - - if ( el[ scroll ] > 0 ) { - return true; - } - - // TODO: determine which cases actually cause this to happen - // if the element doesn't have the scroll set, see if it's possible to - // set the scroll - el[ scroll ] = 1; - has = ( el[ scroll ] > 0 ); - el[ scroll ] = 0; - return has; - } -}); - -})( jQuery ); -(function( $, undefined ) { - -var uuid = 0, - slice = Array.prototype.slice, - _cleanData = $.cleanData; -$.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - } - _cleanData( elems ); -}; - -$.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; - - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; - - if ( !prototype ) { - prototype = base; - base = $.Widget; - } - - // create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { - return !!$.data( elem, fullName ); - }; - - $[ namespace ] = $[ namespace ] || {}; - existingConstructor = $[ namespace ][ name ]; - constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword - if ( !this._createWidget ) { - return new constructor( options, element ); - } - - // allow instantiation without initializing for simple inheritance - // must use "new" keyword (the code above always passes args) - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - // extend with the existing constructor to carry over any static properties - $.extend( constructor, existingConstructor, { - version: prototype.version, - // copy the object used to create the prototype in case we need to - // redefine the widget later - _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is - // redefined after a widget inherits from it - _childConstructors: [] - }); - - basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from - basePrototype.options = $.widget.extend( {}, basePrototype.options ); - $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { - proxiedPrototype[ prop ] = value; - return; - } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; - return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; - - this._super = _super; - this._superApply = _superApply; - - returnValue = value.apply( this, arguments ); - - this._super = __super; - this._superApply = __superApply; - - return returnValue; - }; - })(); - }); - constructor.prototype = $.widget.extend( basePrototype, { - // TODO: remove support for widgetEventPrefix - // always use the name + a colon as the prefix, e.g., draggable:start - // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name - }, proxiedPrototype, { - constructor: constructor, - namespace: namespace, - widgetName: name, - widgetFullName: fullName - }); - - // If this widget is being redefined then we need to find all widgets that - // are inheriting from it and redefine all of them so that they inherit from - // the new version of this widget. We're essentially trying to replace one - // level in the prototype chain. - if ( existingConstructor ) { - $.each( existingConstructor._childConstructors, function( i, child ) { - var childPrototype = child.prototype; - - // redefine the child widget using the same prototype that was - // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor - // so the old child constructors can be garbage collected - delete existingConstructor._childConstructors; - } else { - base._childConstructors.push( constructor ); - } - - $.widget.bridge( name, constructor ); -}; - -$.widget.extend = function( target ) { - var input = slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; - for ( ; inputIndex < inputLength; inputIndex++ ) { - for ( key in input[ inputIndex ] ) { - value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - // Clone objects - if ( $.isPlainObject( value ) ) { - target[ key ] = $.isPlainObject( target[ key ] ) ? - $.widget.extend( {}, target[ key ], value ) : - // Don't extend strings, arrays, etc. with objects - $.widget.extend( {}, value ); - // Copy everything else by reference - } else { - target[ key ] = value; - } - } - } - } - return target; -}; - -$.widget.bridge = function( name, object ) { - var fullName = object.prototype.widgetFullName || name; - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = slice.call( arguments, 1 ), - returnValue = this; - - // allow multiple hashes to be passed on init - options = !isMethodCall && args.length ? - $.widget.extend.apply( null, [ options ].concat(args) ) : - options; - - if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); - } else { - this.each(function() { - var instance = $.data( this, fullName ); - if ( instance ) { - instance.option( options || {} )._init(); - } else { - $.data( this, fullName, new object( options, this ) ); - } - }); - } - - return returnValue; - }; -}; - -$.Widget = function( /* options, element */ ) {}; -$.Widget._childConstructors = []; - -$.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - defaultElement: "
        ", - options: { - disabled: false, - - // callbacks - create: null - }, - _createWidget: function( options, element ) { - element = $( element || this.defaultElement || this )[ 0 ]; - this.element = $( element ); - this.uuid = uuid++; - this.eventNamespace = "." + this.widgetName + this.uuid; - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); - - this.bindings = $(); - this.hoverable = $(); - this.focusable = $(); - - if ( element !== this ) { - $.data( element, this.widgetFullName, this ); - this._on( true, this.element, { - remove: function( event ) { - if ( event.target === element ) { - this.destroy(); - } - } - }); - this.document = $( element.style ? - // element within the document - element.ownerDocument : - // element is window or document - element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); - } - - this._create(); - this._trigger( "create", null, this._getCreateEventData() ); - this._init(); - }, - _getCreateOptions: $.noop, - _getCreateEventData: $.noop, - _create: $.noop, - _init: $.noop, - - destroy: function() { - this._destroy(); - // we can probably remove the unbind calls in 2.0 - // all event bindings should go through this._on() - this.element - .unbind( this.eventNamespace ) - // 1.9 BC for #7810 - // TODO remove dual storage - .removeData( this.widgetName ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // http://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); - this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); - - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - }, - _destroy: $.noop, - - widget: function() { - return this.element; - }, - - option: function( key, value ) { - var options = key, - parts, - curOption, - i; - - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.widget.extend( {}, this.options ); - } - - if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } - options = {}; - parts = key.split( "." ); - key = parts.shift(); - if ( parts.length ) { - curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); - for ( i = 0; i < parts.length - 1; i++ ) { - curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; - curOption = curOption[ parts[ i ] ]; - } - key = parts.pop(); - if ( value === undefined ) { - return curOption[ key ] === undefined ? null : curOption[ key ]; - } - curOption[ key ] = value; - } else { - if ( value === undefined ) { - return this.options[ key ] === undefined ? null : this.options[ key ]; - } - options[ key ] = value; - } - } - - this._setOptions( options ); - - return this; - }, - _setOptions: function( options ) { - var key; - - for ( key in options ) { - this._setOption( key, options[ key ] ); - } - - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; - - if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) - .attr( "aria-disabled", value ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } - - return this; - }, - - enable: function() { - return this._setOption( "disabled", false ); - }, - disable: function() { - return this._setOption( "disabled", true ); - }, - - _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; - - // no suppressDisabledCheck flag, shuffle arguments - if ( typeof suppressDisabledCheck !== "boolean" ) { - handlers = element; - element = suppressDisabledCheck; - suppressDisabledCheck = false; - } - - // no element argument, shuffle and use this.element - if ( !handlers ) { - handlers = element; - element = this.element; - delegateElement = this.widget(); - } else { - // accept selectors, DOM elements - element = delegateElement = $( element ); - this.bindings = this.bindings.add( element ); - } - - $.each( handlers, function( event, handler ) { - function handlerProxy() { - // allow widgets to customize the disabled handling - // - disabled as an array instead of boolean - // - disabled class as method for disabling individual parts - if ( !suppressDisabledCheck && - ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { - return; - } - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - - // copy the guid so direct unbinding works - if ( typeof handler !== "string" ) { - handlerProxy.guid = handler.guid = - handler.guid || handlerProxy.guid || $.guid++; - } - - var match = event.match( /^(\w+)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; - if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); - } else { - element.bind( eventName, handlerProxy ); - } - }); - }, - - _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); - }, - - _delay: function( handler, delay ) { - function handlerProxy() { - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - var instance = this; - return setTimeout( handlerProxy, delay || 0 ); - }, - - _hoverable: function( element ) { - this.hoverable = this.hoverable.add( element ); - this._on( element, { - mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); - }, - mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); - } - }); - }, - - _focusable: function( element ) { - this.focusable = this.focusable.add( element ); - this._on( element, { - focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); - }, - focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); - } - }); - }, - - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; - - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; - - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } - - this.element.trigger( event, data ); - return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || - event.isDefaultPrevented() ); - } -}; - -$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { - $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { - if ( typeof options === "string" ) { - options = { effect: options }; - } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; - options = options || {}; - if ( typeof options === "number" ) { - options = { duration: options }; - } - hasOptions = !$.isEmptyObject( options ); - options.complete = callback; - if ( options.delay ) { - element.delay( options.delay ); - } - if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { - element[ method ]( options ); - } else if ( effectName !== method && element[ effectName ] ) { - element[ effectName ]( options.duration, options.easing, callback ); - } else { - element.queue(function( next ) { - $( this )[ method ](); - if ( callback ) { - callback.call( element[ 0 ] ); - } - next(); - }); - } - }; -}); - -})( jQuery ); -(function( $, undefined ) { - -var mouseHandled = false; -$( document ).mouseup( function() { - mouseHandled = false; -}); - -$.widget("ui.mouse", { - version: "1.10.3", - options: { - cancel: "input,textarea,button,select,option", - distance: 1, - delay: 0 - }, - _mouseInit: function() { - var that = this; - - this.element - .bind("mousedown."+this.widgetName, function(event) { - return that._mouseDown(event); - }) - .bind("click."+this.widgetName, function(event) { - if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { - $.removeData(event.target, that.widgetName + ".preventClickEvent"); - event.stopImmediatePropagation(); - return false; - } - }); - - this.started = false; - }, - - // TODO: make sure destroying one instance of mouse doesn't mess with - // other instances of mouse - _mouseDestroy: function() { - this.element.unbind("."+this.widgetName); - if ( this._mouseMoveDelegate ) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - } - }, - - _mouseDown: function(event) { - // don't let more than one widget handle mouseStart - if( mouseHandled ) { return; } - - // we may have missed mouseup (out of window) - (this._mouseStarted && this._mouseUp(event)); - - this._mouseDownEvent = event; - - var that = this, - btnIsLeft = (event.which === 1), - // event.target.nodeName works around a bug in IE 8 with - // disabled inputs (#7620) - elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); - if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { - return true; - } - - this.mouseDelayMet = !this.options.delay; - if (!this.mouseDelayMet) { - this._mouseDelayTimer = setTimeout(function() { - that.mouseDelayMet = true; - }, this.options.delay); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = (this._mouseStart(event) !== false); - if (!this._mouseStarted) { - event.preventDefault(); - return true; - } - } - - // Click event may never have fired (Gecko & Opera) - if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { - $.removeData(event.target, this.widgetName + ".preventClickEvent"); - } - - // these delegates are required to keep context - this._mouseMoveDelegate = function(event) { - return that._mouseMove(event); - }; - this._mouseUpDelegate = function(event) { - return that._mouseUp(event); - }; - $(document) - .bind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .bind("mouseup."+this.widgetName, this._mouseUpDelegate); - - event.preventDefault(); - - mouseHandled = true; - return true; - }, - - _mouseMove: function(event) { - // IE mouseup check - mouseup happened when mouse was out of window - if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { - return this._mouseUp(event); - } - - if (this._mouseStarted) { - this._mouseDrag(event); - return event.preventDefault(); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = - (this._mouseStart(this._mouseDownEvent, event) !== false); - (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); - } - - return !this._mouseStarted; - }, - - _mouseUp: function(event) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - - if (this._mouseStarted) { - this._mouseStarted = false; - - if (event.target === this._mouseDownEvent.target) { - $.data(event.target, this.widgetName + ".preventClickEvent", true); - } - - this._mouseStop(event); - } - - return false; - }, - - _mouseDistanceMet: function(event) { - return (Math.max( - Math.abs(this._mouseDownEvent.pageX - event.pageX), - Math.abs(this._mouseDownEvent.pageY - event.pageY) - ) >= this.options.distance - ); - }, - - _mouseDelayMet: function(/* event */) { - return this.mouseDelayMet; - }, - - // These are placeholder methods, to be overriden by extending plugin - _mouseStart: function(/* event */) {}, - _mouseDrag: function(/* event */) {}, - _mouseStop: function(/* event */) {}, - _mouseCapture: function(/* event */) { return true; } -}); - -})(jQuery); -(function( $, undefined ) { - -/*jshint loopfunc: true */ - -function isOverAxis( x, reference, size ) { - return ( x > reference ) && ( x < ( reference + size ) ); -} - -function isFloating(item) { - return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); -} - -$.widget("ui.sortable", $.ui.mouse, { - version: "1.10.3", - widgetEventPrefix: "sort", - ready: false, - options: { - appendTo: "parent", - axis: false, - connectWith: false, - containment: false, - cursor: "auto", - cursorAt: false, - dropOnEmpty: true, - forcePlaceholderSize: false, - forceHelperSize: false, - grid: false, - handle: false, - helper: "original", - items: "> *", - opacity: false, - placeholder: false, - revert: false, - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - scope: "default", - tolerance: "intersect", - zIndex: 1000, - - // callbacks - activate: null, - beforeStop: null, - change: null, - deactivate: null, - out: null, - over: null, - receive: null, - remove: null, - sort: null, - start: null, - stop: null, - update: null - }, - _create: function() { - - var o = this.options; - this.containerCache = {}; - this.element.addClass("ui-sortable"); - - //Get the items - this.refresh(); - - //Let's determine if the items are being displayed horizontally - this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; - - //Let's determine the parent's offset - this.offset = this.element.offset(); - - //Initialize mouse events for interaction - this._mouseInit(); - - //We're ready to go - this.ready = true; - - }, - - _destroy: function() { - this.element - .removeClass("ui-sortable ui-sortable-disabled"); - this._mouseDestroy(); - - for ( var i = this.items.length - 1; i >= 0; i-- ) { - this.items[i].item.removeData(this.widgetName + "-item"); - } - - return this; - }, - - _setOption: function(key, value){ - if ( key === "disabled" ) { - this.options[ key ] = value; - - this.widget().toggleClass( "ui-sortable-disabled", !!value ); - } else { - // Don't call widget base _setOption for disable as it adds ui-state-disabled class - $.Widget.prototype._setOption.apply(this, arguments); - } - }, - - _mouseCapture: function(event, overrideHandle) { - var currentItem = null, - validHandle = false, - that = this; - - if (this.reverting) { - return false; - } - - if(this.options.disabled || this.options.type === "static") { - return false; - } - - //We have to refresh the items data once first - this._refreshItems(event); - - //Find out if the clicked node (or one of its parents) is a actual item in this.items - $(event.target).parents().each(function() { - if($.data(this, that.widgetName + "-item") === that) { - currentItem = $(this); - return false; - } - }); - if($.data(event.target, that.widgetName + "-item") === that) { - currentItem = $(event.target); - } - - if(!currentItem) { - return false; - } - if(this.options.handle && !overrideHandle) { - $(this.options.handle, currentItem).find("*").addBack().each(function() { - if(this === event.target) { - validHandle = true; - } - }); - if(!validHandle) { - return false; - } - } - - this.currentItem = currentItem; - this._removeCurrentsFromItems(); - return true; - - }, - - _mouseStart: function(event, overrideHandle, noActivation) { - - var i, body, - o = this.options; - - this.currentContainer = this; - - //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture - this.refreshPositions(); - - //Create and append the visible helper - this.helper = this._createHelper(event); - - //Cache the helper size - this._cacheHelperProportions(); - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Get the next scrolling parent - this.scrollParent = this.helper.scrollParent(); - - //The element's absolute position on the page minus margins - this.offset = this.currentItem.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; - - $.extend(this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper - }); - - // Only after we got the offset, we can change the helper's position to absolute - // TODO: Still need to figure out a way to make relative sorting possible - this.helper.css("position", "absolute"); - this.cssPosition = this.helper.css("position"); - - //Generate the original position - this.originalPosition = this._generatePosition(event); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); - - //Cache the former DOM position - this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; - - //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way - if(this.helper[0] !== this.currentItem[0]) { - this.currentItem.hide(); - } - - //Create the placeholder - this._createPlaceholder(); - - //Set a containment if given in the options - if(o.containment) { - this._setContainment(); - } - - if( o.cursor && o.cursor !== "auto" ) { // cursor option - body = this.document.find( "body" ); - - // support: IE - this.storedCursor = body.css( "cursor" ); - body.css( "cursor", o.cursor ); - - this.storedStylesheet = $( "" ).appendTo( body ); - } - - if(o.opacity) { // opacity option - if (this.helper.css("opacity")) { - this._storedOpacity = this.helper.css("opacity"); - } - this.helper.css("opacity", o.opacity); - } - - if(o.zIndex) { // zIndex option - if (this.helper.css("zIndex")) { - this._storedZIndex = this.helper.css("zIndex"); - } - this.helper.css("zIndex", o.zIndex); - } - - //Prepare scrolling - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - this.overflowOffset = this.scrollParent.offset(); - } - - //Call callbacks - this._trigger("start", event, this._uiHash()); - - //Recache the helper size - if(!this._preserveHelperProportions) { - this._cacheHelperProportions(); - } - - - //Post "activate" events to possible containers - if( !noActivation ) { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); - } - } - - //Prepare possible droppables - if($.ui.ddmanager) { - $.ui.ddmanager.current = this; - } - - if ($.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - - this.dragging = true; - - this.helper.addClass("ui-sortable-helper"); - this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position - return true; - - }, - - _mouseDrag: function(event) { - var i, item, itemElement, intersection, - o = this.options, - scrolled = false; - - //Compute the helpers position - this.position = this._generatePosition(event); - this.positionAbs = this._convertPositionTo("absolute"); - - if (!this.lastPositionAbs) { - this.lastPositionAbs = this.positionAbs; - } - - //Do scrolling - if(this.options.scroll) { - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - - if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; - } - - if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; - } - - } else { - - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); - } - - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); - } - - } - - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - } - - //Regenerate the absolute position used for position checks - this.positionAbs = this._convertPositionTo("absolute"); - - //Set the helper position - if(!this.options.axis || this.options.axis !== "y") { - this.helper[0].style.left = this.position.left+"px"; - } - if(!this.options.axis || this.options.axis !== "x") { - this.helper[0].style.top = this.position.top+"px"; - } - - //Rearrange - for (i = this.items.length - 1; i >= 0; i--) { - - //Cache variables and intersection, continue if no intersection - item = this.items[i]; - itemElement = item.item[0]; - intersection = this._intersectsWithPointer(item); - if (!intersection) { - continue; - } - - // Only put the placeholder inside the current Container, skip all - // items form other containers. This works because when moving - // an item from one container to another the - // currentContainer is switched before the placeholder is moved. - // - // Without this moving items in "sub-sortables" can cause the placeholder to jitter - // beetween the outer and inner container. - if (item.instance !== this.currentContainer) { - continue; - } - - // cannot intersect with itself - // no useless actions that have been done before - // no action if the item moved is the parent of the item checked - if (itemElement !== this.currentItem[0] && - this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && - !$.contains(this.placeholder[0], itemElement) && - (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) - ) { - - this.direction = intersection === 1 ? "down" : "up"; - - if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { - this._rearrange(event, item); - } else { - break; - } - - this._trigger("change", event, this._uiHash()); - break; - } - } - - //Post events to containers - this._contactContainers(event); - - //Interconnect with droppables - if($.ui.ddmanager) { - $.ui.ddmanager.drag(this, event); - } - - //Call callbacks - this._trigger("sort", event, this._uiHash()); - - this.lastPositionAbs = this.positionAbs; - return false; - - }, - - _mouseStop: function(event, noPropagation) { - - if(!event) { - return; - } - - //If we are using droppables, inform the manager about the drop - if ($.ui.ddmanager && !this.options.dropBehaviour) { - $.ui.ddmanager.drop(this, event); - } - - if(this.options.revert) { - var that = this, - cur = this.placeholder.offset(), - axis = this.options.axis, - animation = {}; - - if ( !axis || axis === "x" ) { - animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); - } - if ( !axis || axis === "y" ) { - animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); - } - this.reverting = true; - $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { - that._clear(event); - }); - } else { - this._clear(event, noPropagation); - } - - return false; - - }, - - cancel: function() { - - if(this.dragging) { - - this._mouseUp({ target: null }); - - if(this.options.helper === "original") { - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - //Post deactivating events to containers - for (var i = this.containers.length - 1; i >= 0; i--){ - this.containers[i]._trigger("deactivate", null, this._uiHash(this)); - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", null, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - if (this.placeholder) { - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - if(this.placeholder[0].parentNode) { - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - } - if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { - this.helper.remove(); - } - - $.extend(this, { - helper: null, - dragging: false, - reverting: false, - _noFinalSort: null - }); - - if(this.domPosition.prev) { - $(this.domPosition.prev).after(this.currentItem); - } else { - $(this.domPosition.parent).prepend(this.currentItem); - } - } - - return this; - - }, - - serialize: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - str = []; - o = o || {}; - - $(items).each(function() { - var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); - if (res) { - str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); - } - }); - - if(!str.length && o.key) { - str.push(o.key + "="); - } - - return str.join("&"); - - }, - - toArray: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - ret = []; - - o = o || {}; - - items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); - return ret; - - }, - - /* Be careful with the following core functions */ - _intersectsWith: function(item) { - - var x1 = this.positionAbs.left, - x2 = x1 + this.helperProportions.width, - y1 = this.positionAbs.top, - y2 = y1 + this.helperProportions.height, - l = item.left, - r = l + item.width, - t = item.top, - b = t + item.height, - dyClick = this.offset.click.top, - dxClick = this.offset.click.left, - isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), - isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( this.options.tolerance === "pointer" || - this.options.forcePointerForContainers || - (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) - ) { - return isOverElement; - } else { - - return (l < x1 + (this.helperProportions.width / 2) && // Right Half - x2 - (this.helperProportions.width / 2) < r && // Left Half - t < y1 + (this.helperProportions.height / 2) && // Bottom Half - y2 - (this.helperProportions.height / 2) < b ); // Top Half - - } - }, - - _intersectsWithPointer: function(item) { - - var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), - isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), - isOverElement = isOverElementHeight && isOverElementWidth, - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (!isOverElement) { - return false; - } - - return this.floating ? - ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) - : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); - - }, - - _intersectsWithSides: function(item) { - - var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), - isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (this.floating && horizontalDirection) { - return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); - } else { - return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); - } - - }, - - _getDragVerticalDirection: function() { - var delta = this.positionAbs.top - this.lastPositionAbs.top; - return delta !== 0 && (delta > 0 ? "down" : "up"); - }, - - _getDragHorizontalDirection: function() { - var delta = this.positionAbs.left - this.lastPositionAbs.left; - return delta !== 0 && (delta > 0 ? "right" : "left"); - }, - - refresh: function(event) { - this._refreshItems(event); - this.refreshPositions(); - return this; - }, - - _connectWith: function() { - var options = this.options; - return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; - }, - - _getItemsAsjQuery: function(connected) { - - var i, j, cur, inst, - items = [], - queries = [], - connectWith = this._connectWith(); - - if(connectWith && connected) { - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for ( j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); - } - } - } - } - - queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); - - for (i = queries.length - 1; i >= 0; i--){ - queries[i][0].each(function() { - items.push(this); - }); - } - - return $(items); - - }, - - _removeCurrentsFromItems: function() { - - var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); - - this.items = $.grep(this.items, function (item) { - for (var j=0; j < list.length; j++) { - if(list[j] === item.item[0]) { - return false; - } - } - return true; - }); - - }, - - _refreshItems: function(event) { - - this.items = []; - this.containers = [this]; - - var i, j, cur, inst, targetData, _queries, item, queriesLength, - items = this.items, - queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], - connectWith = this._connectWith(); - - if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for (j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); - this.containers.push(inst); - } - } - } - } - - for (i = queries.length - 1; i >= 0; i--) { - targetData = queries[i][1]; - _queries = queries[i][0]; - - for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { - item = $(_queries[j]); - - item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) - - items.push({ - item: item, - instance: targetData, - width: 0, height: 0, - left: 0, top: 0 - }); - } - } - - }, - - refreshPositions: function(fast) { - - //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change - if(this.offsetParent && this.helper) { - this.offset.parent = this._getParentOffset(); - } - - var i, item, t, p; - - for (i = this.items.length - 1; i >= 0; i--){ - item = this.items[i]; - - //We ignore calculating positions of all connected containers when we're not over them - if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { - continue; - } - - t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; - - if (!fast) { - item.width = t.outerWidth(); - item.height = t.outerHeight(); - } - - p = t.offset(); - item.left = p.left; - item.top = p.top; - } - - if(this.options.custom && this.options.custom.refreshContainers) { - this.options.custom.refreshContainers.call(this); - } else { - for (i = this.containers.length - 1; i >= 0; i--){ - p = this.containers[i].element.offset(); - this.containers[i].containerCache.left = p.left; - this.containers[i].containerCache.top = p.top; - this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); - this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); - } - } - - return this; - }, - - _createPlaceholder: function(that) { - that = that || this; - var className, - o = that.options; - - if(!o.placeholder || o.placeholder.constructor === String) { - className = o.placeholder; - o.placeholder = { - element: function() { - - var nodeName = that.currentItem[0].nodeName.toLowerCase(), - element = $( "<" + nodeName + ">", that.document[0] ) - .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") - .removeClass("ui-sortable-helper"); - - if ( nodeName === "tr" ) { - that.currentItem.children().each(function() { - $( " ", that.document[0] ) - .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) - .appendTo( element ); - }); - } else if ( nodeName === "img" ) { - element.attr( "src", that.currentItem.attr( "src" ) ); - } - - if ( !className ) { - element.css( "visibility", "hidden" ); - } - - return element; - }, - update: function(container, p) { - - // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that - // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified - if(className && !o.forcePlaceholderSize) { - return; - } - - //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item - if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); } - if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); } - } - }; - } - - //Create the placeholder - that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); - - //Append it after the actual current item - that.currentItem.after(that.placeholder); - - //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) - o.placeholder.update(that, that.placeholder); - - }, - - _contactContainers: function(event) { - var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, - innermostContainer = null, - innermostIndex = null; - - // get innermost container that intersects with item - for (i = this.containers.length - 1; i >= 0; i--) { - - // never consider a container that's located within the item itself - if($.contains(this.currentItem[0], this.containers[i].element[0])) { - continue; - } - - if(this._intersectsWith(this.containers[i].containerCache)) { - - // if we've already found a container and it's more "inner" than this, then continue - if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { - continue; - } - - innermostContainer = this.containers[i]; - innermostIndex = i; - - } else { - // container doesn't intersect. trigger "out" event if necessary - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", event, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - // if no intersecting containers found, return - if(!innermostContainer) { - return; - } - - // move the item into the container if it's not there already - if(this.containers.length === 1) { - if (!this.containers[innermostIndex].containerCache.over) { - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - } else { - - //When entering a new container, we will find the item with the least distance and append our item near it - dist = 10000; - itemWithLeastDistance = null; - floating = innermostContainer.floating || isFloating(this.currentItem); - posProperty = floating ? "left" : "top"; - sizeProperty = floating ? "width" : "height"; - base = this.positionAbs[posProperty] + this.offset.click[posProperty]; - for (j = this.items.length - 1; j >= 0; j--) { - if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { - continue; - } - if(this.items[j].item[0] === this.currentItem[0]) { - continue; - } - if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { - continue; - } - cur = this.items[j].item.offset()[posProperty]; - nearBottom = false; - if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){ - nearBottom = true; - cur += this.items[j][sizeProperty]; - } - - if(Math.abs(cur - base) < dist) { - dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; - this.direction = nearBottom ? "up": "down"; - } - } - - //Check if dropOnEmpty is enabled - if(!itemWithLeastDistance && !this.options.dropOnEmpty) { - return; - } - - if(this.currentContainer === this.containers[innermostIndex]) { - return; - } - - itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); - this._trigger("change", event, this._uiHash()); - this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); - this.currentContainer = this.containers[innermostIndex]; - - //Update the placeholder - this.options.placeholder.update(this.currentContainer, this.placeholder); - - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - - - }, - - _createHelper: function(event) { - - var o = this.options, - helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); - - //Add the helper to the DOM if that didn't happen already - if(!helper.parents("body").length) { - $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); - } - - if(helper[0] === this.currentItem[0]) { - this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; - } - - if(!helper[0].style.width || o.forceHelperSize) { - helper.width(this.currentItem.width()); - } - if(!helper[0].style.height || o.forceHelperSize) { - helper.height(this.currentItem.height()); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function(obj) { - if (typeof obj === "string") { - obj = obj.split(" "); - } - if ($.isArray(obj)) { - obj = {left: +obj[0], top: +obj[1] || 0}; - } - if ("left" in obj) { - this.offset.click.left = obj.left + this.margins.left; - } - if ("right" in obj) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ("top" in obj) { - this.offset.click.top = obj.top + this.margins.top; - } - if ("bottom" in obj) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - - //Get the offsetParent and cache its position - this.offsetParent = this.helper.offsetParent(); - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that - // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag - if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - // This needs to be actually done for all browsers, since pageX/pageY includes this information - // with an ugly IE fix - if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), - left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) - }; - - }, - - _getRelativeOffset: function() { - - if(this.cssPosition === "relative") { - var p = this.currentItem.position(); - return { - top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), - left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), - top: (parseInt(this.currentItem.css("marginTop"),10) || 0) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var ce, co, over, - o = this.options; - if(o.containment === "parent") { - o.containment = this.helper[0].parentNode; - } - if(o.containment === "document" || o.containment === "window") { - this.containment = [ - 0 - this.offset.relative.left - this.offset.parent.left, - 0 - this.offset.relative.top - this.offset.parent.top, - $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, - ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top - ]; - } - - if(!(/^(document|window|parent)$/).test(o.containment)) { - ce = $(o.containment)[0]; - co = $(o.containment).offset(); - over = ($(ce).css("overflow") !== "hidden"); - - this.containment = [ - co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, - co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, - co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, - co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top - ]; - } - - }, - - _convertPositionTo: function(d, pos) { - - if(!pos) { - pos = this.position; - } - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, - scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - return { - top: ( - pos.top + // The absolute mouse position - this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod) - ), - left: ( - pos.left + // The absolute mouse position - this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod) - ) - }; - - }, - - _generatePosition: function(event) { - - var top, left, - o = this.options, - pageX = event.pageX, - pageY = event.pageY, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - // This is another very weird special case that only happens for relative elements: - // 1. If the css position is relative - // 2. and the scroll parent is the document or similar to the offset parent - // we have to refresh the relative offset during the scroll so there are no jumps - if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { - this.offset.relative = this._getRelativeOffset(); - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - if(this.originalPosition) { //If we are not dragging yet, we won't check for options - - if(this.containment) { - if(event.pageX - this.offset.click.left < this.containment[0]) { - pageX = this.containment[0] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top < this.containment[1]) { - pageY = this.containment[1] + this.offset.click.top; - } - if(event.pageX - this.offset.click.left > this.containment[2]) { - pageX = this.containment[2] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top > this.containment[3]) { - pageY = this.containment[3] + this.offset.click.top; - } - } - - if(o.grid) { - top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; - pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; - - left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; - pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; - } - - } - - return { - top: ( - pageY - // The absolute mouse position - this.offset.click.top - // Click offset (relative to the element) - this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) )) - ), - left: ( - pageX - // The absolute mouse position - this.offset.click.left - // Click offset (relative to the element) - this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() )) - ) - }; - - }, - - _rearrange: function(event, i, a, hardRefresh) { - - a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); - - //Various things done here to improve the performance: - // 1. we create a setTimeout, that calls refreshPositions - // 2. on the instance, we have a counter variable, that get's higher after every append - // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same - // 4. this lets only the last addition to the timeout stack through - this.counter = this.counter ? ++this.counter : 1; - var counter = this.counter; - - this._delay(function() { - if(counter === this.counter) { - this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove - } - }); - - }, - - _clear: function(event, noPropagation) { - - this.reverting = false; - // We delay all events that have to be triggered to after the point where the placeholder has been removed and - // everything else normalized again - var i, - delayedTriggers = []; - - // We first have to update the dom position of the actual currentItem - // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) - if(!this._noFinalSort && this.currentItem.parent().length) { - this.placeholder.before(this.currentItem); - } - this._noFinalSort = null; - - if(this.helper[0] === this.currentItem[0]) { - for(i in this._storedCSS) { - if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { - this._storedCSS[i] = ""; - } - } - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - if(this.fromOutside && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); - } - if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed - } - - // Check if the items Container has Changed and trigger appropriate - // events. - if (this !== this.currentContainer) { - if(!noPropagation) { - delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); - delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - } - } - - - //Post events to containers - for (i = this.containers.length - 1; i >= 0; i--){ - if(!noPropagation) { - delayedTriggers.push((function(c) { return function(event) { c._trigger("deactivate", event, this._uiHash(this)); }; }).call(this, this.containers[i])); - } - if(this.containers[i].containerCache.over) { - delayedTriggers.push((function(c) { return function(event) { c._trigger("out", event, this._uiHash(this)); }; }).call(this, this.containers[i])); - this.containers[i].containerCache.over = 0; - } - } - - //Do what was originally in plugins - if ( this.storedCursor ) { - this.document.find( "body" ).css( "cursor", this.storedCursor ); - this.storedStylesheet.remove(); - } - if(this._storedOpacity) { - this.helper.css("opacity", this._storedOpacity); - } - if(this._storedZIndex) { - this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); - } - - this.dragging = false; - if(this.cancelHelperRemoval) { - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return false; - } - - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - } - - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - - if(this.helper[0] !== this.currentItem[0]) { - this.helper.remove(); - } - this.helper = null; - - if(!noPropagation) { - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return true; - - }, - - _trigger: function() { - if ($.Widget.prototype._trigger.apply(this, arguments) === false) { - this.cancel(); - } - }, - - _uiHash: function(_inst) { - var inst = _inst || this; - return { - helper: inst.helper, - placeholder: inst.placeholder || $([]), - position: inst.position, - originalPosition: inst.originalPosition, - offset: inst.positionAbs, - item: inst.currentItem, - sender: _inst ? _inst.element : null - }; - } - -}); - -})(jQuery); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery.blockUI.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery.blockUI.js deleted file mode 100644 index 90ce5d64d..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery.blockUI.js +++ /dev/null @@ -1,620 +0,0 @@ -/*! - * jQuery blockUI plugin - * Version 2.70.0-2014.11.23 - * Requires jQuery v1.7 or later - * - * Examples at: http://malsup.com/jquery/block/ - * Copyright (c) 2007-2013 M. Alsup - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Thanks to Amir-Hossein Sobhi for some excellent contributions! - */ - -;(function() { -/*jshint eqeqeq:false curly:false latedef:false */ -"use strict"; - - function setup($) { - $.fn._fadeIn = $.fn.fadeIn; - - var noOp = $.noop || function() {}; - - // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle - // confusing userAgent strings on Vista) - var msie = /MSIE/.test(navigator.userAgent); - var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent); - var mode = document.documentMode || 0; - var setExpr = $.isFunction( document.createElement('div').style.setExpression ); - - // global $ methods for blocking/unblocking the entire page - $.blockUI = function(opts) { install(window, opts); }; - $.unblockUI = function(opts) { remove(window, opts); }; - - // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) - $.growlUI = function(title, message, timeout, onClose) { - var $m = $('
        '); - if (title) $m.append('

        '+title+'

        '); - if (message) $m.append('

        '+message+'

        '); - if (timeout === undefined) timeout = 3000; - - // Added by konapun: Set timeout to 30 seconds if this growl is moused over, like normal toast notifications - var callBlock = function(opts) { - opts = opts || {}; - - $.blockUI({ - message: $m, - fadeIn : typeof opts.fadeIn !== 'undefined' ? opts.fadeIn : 700, - fadeOut: typeof opts.fadeOut !== 'undefined' ? opts.fadeOut : 1000, - timeout: typeof opts.timeout !== 'undefined' ? opts.timeout : timeout, - centerY: false, - showOverlay: false, - onUnblock: onClose, - css: $.blockUI.defaults.growlCSS - }); - }; - - callBlock(); - var nonmousedOpacity = $m.css('opacity'); - $m.mouseover(function() { - callBlock({ - fadeIn: 0, - timeout: 30000 - }); - - var displayBlock = $('.blockMsg'); - displayBlock.stop(); // cancel fadeout if it has started - displayBlock.fadeTo(300, 1); // make it easier to read the message by removing transparency - }).mouseout(function() { - $('.blockMsg').fadeOut(1000); - }); - // End konapun additions - }; - - // plugin method for blocking element content - $.fn.block = function(opts) { - if ( this[0] === window ) { - $.blockUI( opts ); - return this; - } - var fullOpts = $.extend({}, $.blockUI.defaults, opts || {}); - this.each(function() { - var $el = $(this); - if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked')) - return; - $el.unblock({ fadeOut: 0 }); - }); - - return this.each(function() { - if ($.css(this,'position') == 'static') { - this.style.position = 'relative'; - $(this).data('blockUI.static', true); - } - this.style.zoom = 1; // force 'hasLayout' in ie - install(this, opts); - }); - }; - - // plugin method for unblocking element content - $.fn.unblock = function(opts) { - if ( this[0] === window ) { - $.unblockUI( opts ); - return this; - } - return this.each(function() { - remove(this, opts); - }); - }; - - $.blockUI.version = 2.70; // 2nd generation blocking at no extra cost! - - // override these in your code to change the default behavior and style - $.blockUI.defaults = { - // message displayed when blocking (use null for no message) - message: '

        Please wait...

        ', - - title: null, // title string; only used when theme == true - draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) - - theme: false, // set to true to use with jQuery UI themes - - // styles for the message when blocking; if you wish to disable - // these and use an external stylesheet then do this in your code: - // $.blockUI.defaults.css = {}; - css: { - padding: 0, - margin: 0, - width: '30%', - top: '40%', - left: '35%', - textAlign: 'center', - color: '#000', - border: '3px solid #aaa', - backgroundColor:'#fff', - cursor: 'wait' - }, - - // minimal style set used when themes are used - themedCSS: { - width: '30%', - top: '40%', - left: '35%' - }, - - // styles for the overlay - overlayCSS: { - backgroundColor: '#000', - opacity: 0.6, - cursor: 'wait' - }, - - // style to replace wait cursor before unblocking to correct issue - // of lingering wait cursor - cursorReset: 'default', - - // styles applied when using $.growlUI - growlCSS: { - width: '350px', - top: '10px', - left: '', - right: '10px', - border: 'none', - padding: '5px', - opacity: 0.6, - cursor: 'default', - color: '#fff', - backgroundColor: '#000', - '-webkit-border-radius':'10px', - '-moz-border-radius': '10px', - 'border-radius': '10px' - }, - - // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w - // (hat tip to Jorge H. N. de Vasconcelos) - /*jshint scripturl:true */ - iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', - - // force usage of iframe in non-IE browsers (handy for blocking applets) - forceIframe: false, - - // z-index for the blocking overlay - baseZ: 1000, - - // set these to true to have the message automatically centered - centerX: true, // <-- only effects element blocking (page block controlled via css above) - centerY: true, - - // allow body element to be stetched in ie6; this makes blocking look better - // on "short" pages. disable if you wish to prevent changes to the body height - allowBodyStretch: true, - - // enable if you want key and mouse events to be disabled for content that is blocked - bindEvents: true, - - // be default blockUI will supress tab navigation from leaving blocking content - // (if bindEvents is true) - constrainTabKey: true, - - // fadeIn time in millis; set to 0 to disable fadeIn on block - fadeIn: 200, - - // fadeOut time in millis; set to 0 to disable fadeOut on unblock - fadeOut: 400, - - // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock - timeout: 0, - - // disable if you don't want to show the overlay - showOverlay: true, - - // if true, focus will be placed in the first available input field when - // page blocking - focusInput: true, - - // elements that can receive focus - focusableElements: ':input:enabled:visible', - - // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) - // no longer needed in 2012 - // applyPlatformOpacityRules: true, - - // callback method invoked when fadeIn has completed and blocking message is visible - onBlock: null, - - // callback method invoked when unblocking has completed; the callback is - // passed the element that has been unblocked (which is the window object for page - // blocks) and the options that were passed to the unblock call: - // onUnblock(element, options) - onUnblock: null, - - // callback method invoked when the overlay area is clicked. - // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used. - onOverlayClick: null, - - // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 - quirksmodeOffsetHack: 4, - - // class name of the message block - blockMsgClass: 'blockMsg', - - // if it is already blocked, then ignore it (don't unblock and reblock) - ignoreIfBlocked: false - }; - - // private data and functions follow... - - var pageBlock = null; - var pageBlockEls = []; - - function install(el, opts) { - var css, themedCSS; - var full = (el == window); - var msg = (opts && opts.message !== undefined ? opts.message : undefined); - opts = $.extend({}, $.blockUI.defaults, opts || {}); - - if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked')) - return; - - opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); - css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); - if (opts.onOverlayClick) - opts.overlayCSS.cursor = 'pointer'; - - themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); - msg = msg === undefined ? opts.message : msg; - - // remove the current block (if there is one) - if (full && pageBlock) - remove(window, {fadeOut:0}); - - // if an existing element is being used as the blocking content then we capture - // its current place in the DOM (and current display style) so we can restore - // it when we unblock - if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { - var node = msg.jquery ? msg[0] : msg; - var data = {}; - $(el).data('blockUI.history', data); - data.el = node; - data.parent = node.parentNode; - data.display = node.style.display; - data.position = node.style.position; - if (data.parent) - data.parent.removeChild(node); - } - - $(el).data('blockUI.onUnblock', opts.onUnblock); - var z = opts.baseZ; - - // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; - // layer1 is the iframe layer which is used to supress bleed through of underlying content - // layer2 is the overlay layer which has opacity and a wait cursor (by default) - // layer3 is the message content that is displayed while blocking - var lyr1, lyr2, lyr3, s; - if (msie || opts.forceIframe) - lyr1 = $(''); - else - lyr1 = $(''); - - if (opts.theme) - lyr2 = $(''); - else - lyr2 = $(''); - - if (opts.theme && full) { - s = ''; - } - else if (opts.theme) { - s = ''; - } - else if (full) { - s = ''; - } - else { - s = ''; - } - lyr3 = $(s); - - // if we have a message, style it - if (msg) { - if (opts.theme) { - lyr3.css(themedCSS); - lyr3.addClass('ui-widget-content'); - } - else - lyr3.css(css); - } - - // style the overlay - if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/) - lyr2.css(opts.overlayCSS); - lyr2.css('position', full ? 'fixed' : 'absolute'); - - // make iframe layer transparent in IE - if (msie || opts.forceIframe) - lyr1.css('opacity',0.0); - - //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); - var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); - $.each(layers, function() { - this.appendTo($par); - }); - - if (opts.theme && opts.draggable && $.fn.draggable) { - lyr3.draggable({ - handle: '.ui-dialog-titlebar', - cancel: 'li' - }); - } - - // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) - var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0); - if (ie6 || expr) { - // give body 100% height - if (full && opts.allowBodyStretch && $.support.boxModel) - $('html,body').css('height','100%'); - - // fix ie6 issue when blocked element has a border width - if ((ie6 || !$.support.boxModel) && !full) { - var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); - var fixT = t ? '(0 - '+t+')' : 0; - var fixL = l ? '(0 - '+l+')' : 0; - } - - // simulate fixed position - $.each(layers, function(i,o) { - var s = o[0].style; - s.position = 'absolute'; - if (i < 2) { - if (full) - s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"'); - else - s.setExpression('height','this.parentNode.offsetHeight + "px"'); - if (full) - s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'); - else - s.setExpression('width','this.parentNode.offsetWidth + "px"'); - if (fixL) s.setExpression('left', fixL); - if (fixT) s.setExpression('top', fixT); - } - else if (opts.centerY) { - if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); - s.marginTop = 0; - } - else if (!opts.centerY && full) { - var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0; - var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; - s.setExpression('top',expression); - } - }); - } - - // show the message - if (msg) { - if (opts.theme) - lyr3.find('.ui-widget-content').append(msg); - else - lyr3.append(msg); - if (msg.jquery || msg.nodeType) - $(msg).show(); - } - - if ((msie || opts.forceIframe) && opts.showOverlay) - lyr1.show(); // opacity is zero - if (opts.fadeIn) { - var cb = opts.onBlock ? opts.onBlock : noOp; - var cb1 = (opts.showOverlay && !msg) ? cb : noOp; - var cb2 = msg ? cb : noOp; - if (opts.showOverlay) - lyr2._fadeIn(opts.fadeIn, cb1); - if (msg) - lyr3._fadeIn(opts.fadeIn, cb2); - } - else { - if (opts.showOverlay) - lyr2.show(); - if (msg) - lyr3.show(); - if (opts.onBlock) - opts.onBlock.bind(lyr3)(); - } - - // bind key and mouse events - bind(1, el, opts); - - if (full) { - pageBlock = lyr3[0]; - pageBlockEls = $(opts.focusableElements,pageBlock); - if (opts.focusInput) - setTimeout(focus, 20); - } - else - center(lyr3[0], opts.centerX, opts.centerY); - - if (opts.timeout) { - // auto-unblock - var to = setTimeout(function() { - if (full) - $.unblockUI(opts); - else - $(el).unblock(opts); - }, opts.timeout); - $(el).data('blockUI.timeout', to); - } - } - - // remove the block - function remove(el, opts) { - var count; - var full = (el == window); - var $el = $(el); - var data = $el.data('blockUI.history'); - var to = $el.data('blockUI.timeout'); - if (to) { - clearTimeout(to); - $el.removeData('blockUI.timeout'); - } - opts = $.extend({}, $.blockUI.defaults, opts || {}); - bind(0, el, opts); // unbind events - - if (opts.onUnblock === null) { - opts.onUnblock = $el.data('blockUI.onUnblock'); - $el.removeData('blockUI.onUnblock'); - } - - var els; - if (full) // crazy selector to handle odd field errors in ie6/7 - els = $('body').children().filter('.blockUI').add('body > .blockUI'); - else - els = $el.find('>.blockUI'); - - // fix cursor issue - if ( opts.cursorReset ) { - if ( els.length > 1 ) - els[1].style.cursor = opts.cursorReset; - if ( els.length > 2 ) - els[2].style.cursor = opts.cursorReset; - } - - if (full) - pageBlock = pageBlockEls = null; - - if (opts.fadeOut) { - count = els.length; - els.stop().fadeOut(opts.fadeOut, function() { - if ( --count === 0) - reset(els,data,opts,el); - }); - } - else - reset(els, data, opts, el); - } - - // move blocking element back into the DOM where it started - function reset(els,data,opts,el) { - var $el = $(el); - if ( $el.data('blockUI.isBlocked') ) - return; - - els.each(function(i,o) { - // remove via DOM calls so we don't lose event handlers - if (this.parentNode) - this.parentNode.removeChild(this); - }); - - if (data && data.el) { - data.el.style.display = data.display; - data.el.style.position = data.position; - data.el.style.cursor = 'default'; // #59 - if (data.parent) - data.parent.appendChild(data.el); - $el.removeData('blockUI.history'); - } - - if ($el.data('blockUI.static')) { - $el.css('position', 'static'); // #22 - } - - if (typeof opts.onUnblock == 'function') - opts.onUnblock(el,opts); - - // fix issue in Safari 6 where block artifacts remain until reflow - var body = $(document.body), w = body.width(), cssW = body[0].style.width; - body.width(w-1).width(w); - body[0].style.width = cssW; - } - - // bind/unbind the handler - function bind(b, el, opts) { - var full = el == window, $el = $(el); - - // don't bother unbinding if there is nothing to unbind - if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) - return; - - $el.data('blockUI.isBlocked', b); - - // don't bind events when overlay is not in use or if bindEvents is false - if (!full || !opts.bindEvents || (b && !opts.showOverlay)) - return; - - // bind anchors and inputs for mouse and key events - var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove'; - if (b) - $(document).bind(events, opts, handler); - else - $(document).unbind(events, handler); - - // former impl... - // var $e = $('a,:input'); - // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); - } - - // event handler to suppress keyboard/mouse events when blocking - function handler(e) { - // allow tab navigation (conditionally) - if (e.type === 'keydown' && e.keyCode && e.keyCode == 9) { - if (pageBlock && e.data.constrainTabKey) { - var els = pageBlockEls; - var fwd = !e.shiftKey && e.target === els[els.length-1]; - var back = e.shiftKey && e.target === els[0]; - if (fwd || back) { - setTimeout(function(){focus(back);},10); - return false; - } - } - } - var opts = e.data; - var target = $(e.target); - if (target.hasClass('blockOverlay') && opts.onOverlayClick) - opts.onOverlayClick(e); - - // allow events within the message content - if (target.parents('div.' + opts.blockMsgClass).length > 0) - return true; - - // allow events for content that is not being blocked - return target.parents().children().filter('div.blockUI').length === 0; - } - - function focus(back) { - if (!pageBlockEls) - return; - var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; - if (e) - e.focus(); - } - - function center(el, x, y) { - var p = el.parentNode, s = el.style; - var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); - var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); - if (x) s.left = l > 0 ? (l+'px') : '0'; - if (y) s.top = t > 0 ? (t+'px') : '0'; - } - - function sz(el, p) { - return parseInt($.css(el,p),10)||0; - } - - } - - - /*global define:true */ - if (typeof define === 'function' && define.amd && define.amd.jQuery) { - define(['jquery'], setup); - } else { - setup(jQuery); - } - -})(); diff --git a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery.slugify.js b/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery.slugify.js deleted file mode 100644 index 936056586..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/vendor/jquery.slugify.js +++ /dev/null @@ -1,55 +0,0 @@ -(function ($) { - - $.fn.slugify = function (source, options) { - var $target = this; - var $source = $(source); - - var settings = $.extend({ - slugFunc: (function (val, originalFunc) { return originalFunc(val); }) - }, options); - - - var convertToSlug = function(val) { - return settings.slugFunc(val, - (function(v) { - if (!v) return ''; - var from = "ıİöÖüÜçÇğĞşŞâÂêÊîÎôÔûÛ"; - var to = "iIoOuUcCgGsSaAeEiIoOuU"; - - for (var i=0, l=from.length ; i max) { - end = mid - 1; - } else { - start = mid + 1; - } - - return findTruncPoint(dim, max, txt, start, end, $worker, token, reverse); - } - - $.fn.truncate = function (options) { - // backward compatibility - if (options && !!options.center && !options.side) { - options.side = 'center'; - delete options.center; - } - - if (options && !(/^(left|right|center)$/).test(options.side)) { - delete options.side; - } - - var defaults = { - width: 'auto', - token: '…', - side: 'right', - addclass: false, - addtitle: false, - multiline: false, - assumeSameStyle: false - }; - options = $.extend(defaults, options); - - var fontCSS; - var $element; - var $truncateWorker; - var elementText; - - if (options.assumeSameStyle) { - $element = $(this[0]); - fontCSS = { - 'fontFamily': $element.css('fontFamily'), - 'fontSize': $element.css('fontSize'), - 'fontStyle': $element.css('fontStyle'), - 'fontWeight': $element.css('fontWeight'), - 'font-variant': $element.css('font-variant'), - 'text-indent': $element.css('text-indent'), - 'text-transform': $element.css('text-transform'), - 'letter-spacing': $element.css('letter-spacing'), - 'word-spacing': $element.css('word-spacing'), - 'display': 'none' - }; - $truncateWorker = $('') - .css(fontCSS) - .appendTo('body'); - } - - return this.each(function () { - $element = $(this); - elementText = $element.text(); - if (!options.assumeSameStyle) { - fontCSS = { - 'fontFamily': $element.css('fontFamily'), - 'fontSize': $element.css('fontSize'), - 'fontStyle': $element.css('fontStyle'), - 'fontWeight': $element.css('fontWeight'), - 'font-variant': $element.css('font-variant'), - 'text-indent': $element.css('text-indent'), - 'text-transform': $element.css('text-transform'), - 'letter-spacing': $element.css('letter-spacing'), - 'word-spacing': $element.css('word-spacing'), - 'display': 'none' - }; - $truncateWorker = $('') - .css(fontCSS) - .text(elementText) - .appendTo('body'); - } else { - $truncateWorker.text(elementText); - } - - var originalWidth = $truncateWorker.width(); - var truncateWidth = parseInt(options.width, 10) || $element.width(); - var dimension = 'width'; - var truncatedText, originalDim, truncateDim; - - if (options.multiline) { - $truncateWorker.width($element.width()); - dimension = 'height'; - originalDim = $truncateWorker.height(); - truncateDim = $element.height() + 1; - } - else { - originalDim = originalWidth; - truncateDim = truncateWidth; - } - - truncatedText = {before: '', after: ''}; - if (originalDim > truncateDim) { - var truncPoint, truncPoint2; - $truncateWorker.text(''); - - if (options.side === 'left') { - truncPoint = findTruncPoint( - dimension, truncateDim, elementText, 0, elementText.length, - $truncateWorker, options.token, true - ); - truncatedText.after = elementText.slice(-1 * truncPoint); - - } else if (options.side === 'center') { - truncateDim = parseInt(truncateDim / 2, 10) - 1; - truncPoint = findTruncPoint( - dimension, truncateDim, elementText, 0, elementText.length, - $truncateWorker, options.token, false - ); - truncPoint2 = findTruncPoint( - dimension, truncateDim, elementText, 0, elementText.length, - $truncateWorker, '', true - ); - truncatedText.before = elementText.slice(0, truncPoint); - truncatedText.after = elementText.slice(-1 * truncPoint2); - - } else if (options.side === 'right') { - truncPoint = findTruncPoint( - dimension, truncateDim, elementText, 0, elementText.length, - $truncateWorker, options.token, false - ); - truncatedText.before = elementText.slice(0, truncPoint); - } - - if (options.addclass) { - $element.addClass(options.addclass); - } - - if (options.addtitle) { - $element.attr('title', elementText); - } - - truncatedText.before = $truncateWorker - .text(truncatedText - .before).html(); - truncatedText.after = $truncateWorker - .text(truncatedText.after) - .html(); - $element.empty().html( - truncatedText.before + options.token + truncatedText.after - ); - - } - - if (!options.assumeSameStyle) { - $truncateWorker.remove(); - } - }); - - if (options.assumeSameStyle) { - $truncateWorker.remove(); - } - }; -})(jQuery); diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin.css.scss b/src/api-umbrella/web-app/app/assets/stylesheets/admin.css.scss deleted file mode 100644 index c2b75e3c3..000000000 --- a/src/api-umbrella/web-app/app/assets/stylesheets/admin.css.scss +++ /dev/null @@ -1,14 +0,0 @@ -//= require "vendor/datatables-plugins/dataTables.bootstrap" - -@import "admin/variables"; -@import "bootstrap"; -@import "bootstrap-responsive"; -@import "font-awesome"; - -@import "vendor/jQuery-QueryBuilder/query-builder"; -@import "qtip2/jquery.qtip"; -@import "bootstrap-daterangepicker/daterangepicker-bs2"; -@import "selectize/selectize.default"; -@import "pnotify"; -@import "admin/base"; -@import "admin/stats"; diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin/base.css.scss b/src/api-umbrella/web-app/app/assets/stylesheets/admin/base.css.scss deleted file mode 100644 index ebf4eeb57..000000000 --- a/src/api-umbrella/web-app/app/assets/stylesheets/admin/base.css.scss +++ /dev/null @@ -1,580 +0,0 @@ -body { - padding-top: 60px; -} - -.navbar .brand { - font-weight: bold; -} - -.crumbs { - font-size: 85%; - color: #3c4349; - font-weight: bold; - margin-bottom: 12px; -} - -.crumbs a:link, -.crumbs a:visited { - color: #3c4349; -} - - -ul.collection-list { - margin: 0px; -} - -ul.collection-list li { - list-style-type: none; - margin-bottom: 10px; -} - -ul.collection-list ul { - margin: 10px 0px 10px 40px; -} - -ul.collection-list .collection-item { - background-color: #e0e0e3; - padding: 8px 16px; - - -webkit-border-radius: 16px; - -moz-border-radius: 16px; - border-radius: 16px; - - -webkit-box-shadow: 1px 1px 0px 0px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 1px 1px 0px 0px rgba(0, 0, 0, 0.2); - box-shadow: 1px 1px 0px 0px rgba(0, 0, 0, 0.2); -} - -ul.collection-list .collection-actions { - float: right; -} - -.input { - margin: 3px 0px; -} - -.input label { - font-weight: bold; - margin-bottom: 2px; -} - -.hint { - display: block; - clear: left; - color: #777; - font-size: 93%; - padding-top: 2px; -} - -textarea.editor { -} - -input.fill, -textarea.fill { - width: 100%; -} - -.icon { - width: 17px; - height: 17px; - display: block; - float: left; - text-indent: -9999px; -} - -.button-actions { - text-align: right; - margin-bottom: 16px; -} - -.button-actions-down { - position: relative; - height: 0px; - margin: 0px; - z-index: 999; -} - -.select-mini { - font-size: 11px; - height: 22px; - line-height: 22px; - margin-bottom: 0px; - padding-top: 2px; - padding-bottom: 2px; -} - -#results_table { -} - -#results_table .table th, -#results_table .table td { - white-space: nowrap; -} - -#results_table .table thead th { - color: $linkColor; -} - -table.table thead .sorting, -table.table thead .sorting_asc, -table.table thead .sorting_desc { - background: none; -} - -table.table thead .sorting:after, -table.table thead .sorting_asc:after, -table.table thead .sorting_desc:after { - font-family: FontAwesome; - color: #ddd; - margin-left: 8px; -} - -table.table thead .sorting_asc:after, -table.table thead .sorting_desc:after { - color: $linkColor; -} - -table.table thead .sorting:after { - content: "\F0DC"; -} - -table.table thead .sorting_asc:after { - content: "\F0DE"; -} - -table.table thead .sorting_desc:after { - content: "\F0DD"; -} - -div.dataTables_info { - padding: 0px; - font-size: 13px; - color: #666; -} - -div.dataTables_paginate { - text-align: center; - float: none; -} - -div.dataTables_length { - text-align: right; - - label { - float: none; - font-size: 13px; - color: #666; - } - - select { - font-size: 13px; - } -} - -div.dataTables_filter { - label { - float: left; - } -} - -.dataTables_wrapper { - position: relative; - - .blockUI { - color: #999 !important; - border: 1px solid #ddd !important; - font-size: 32px !important; - line-height: 60px !important; - } -} - -.dataTables_processing { - position: absolute; - top: 50%; - left: 50%; - width: 250px; - height: 60px; - margin-left: -125px; - margin-top: -30px; - border: 1px solid #ddd; - text-align: center; - color: #999; - font-size: 32px; - line-height: 60px; - background-color: white; -} - -.loading-overlay { - position: fixed; - top: 0px; - left: 0px; - width: 100%; - height: 100%; - background-color: rgba(100, 100, 100, 0.5); - z-index: 999; -} - -.control-label { - font-weight: bold; - - label { - font-weight: bold; - display: inline; - } -} - -.control-group { - margin-top: 5px; -} - -.form-horizontal .control-group { - margin-bottom: 10px; -} - -.form-horizontal.info .control-group { - margin: 3px 0px 5px 0px; -} - -.form-horizontal.info .control-label { - padding-top: 0px; -} - -.modal form { - margin: 0px; -} - -fieldset { - margin: 12px 0px 32px 0px; - padding: 6px 0px 6px 12px; -} - -fieldset.collapsible { - margin-bottom: 12px; - - .collapse.in { - margin-bottom: 24px; - } -} - -legend { - font-family: 'Oswald', sans-serif; - margin-bottom: 2px; - border-bottom-width: 1px; - border-color: #999; - margin-left: -12px; - padding: 6px 10px 3px 2px; - line-height: 22px; - background-color: #fff; -} - -legend a { - display: block; -} - -.fieldset-note { - font-size: 85%; - color: #555; - margin-bottom: 4px; -} - -fieldset .table { - margin-bottom: 4px; -} - -.control-group.error .help-block { - margin-top: -8px; - font-style: italic; -} - -.form-horizontal .control-group.error .help-block { - margin-top: 0px; -} - -.error-messages { - h4 { - margin-bottom: 4px; - } - - ul { - margin-bottom: 0px; - - ul { - margin-top: 10px; - margin-bottom: 10px; - } - } - - li { - p { - margin: 10px 0px 0px 0px; - } - - p:first-child { - margin: 0px; - } - } -} - -.arrow { - margin-top: 24px; - text-align: center; - color: #999; - font-size: 10px; - line-height: 10px; -} - -.arrow i { - color: #ddd; -} - -.arrow-vertical { - margin: 0px; -} - -.arrow-vertical i { - vertical-align: middle; -} - -a { - .fa { - text-decoration: none; - margin-right: 3px; - } -} - -.fa-space-right { - margin-right: 3px; -} - -.table .table-row-actions { - text-align: right; - white-space: nowrap; - - a { - margin-left: 24px; - } -} - -a.remove-action { - font-size: 12px; - color: $red; -} - -.table-actions { -} - -.form-extra-actions { - margin-top: 20px; -} - -table input { - margin-bottom: 0px !important; -} - -.form-horizontal-wide { - .control-label { - width: 240px; - } - - .controls { - margin-left: 260px; - } -} - -legend a:hover { - text-decoration: none; -} - -a[data-toggle='collapse'] .fa-caret-down:before { - display: block; - width: 16px; - text-align: center; -} - -a[data-toggle='collapse'].collapsed .fa-caret-down:before { - content: "\f0da"; -} - -a:hover { - cursor: pointer; -} - -.modal { - width: 820px; - margin-left: -410px; -} - -.api-key { - font-family: $monoFontFamily; -} - -.selectize-control { - width: 100%; -} - -.selectize-dropdown { - z-index: 9999; -} - -.record-details { - text-align: right; - font-size: 11px; - line-height: 13px; - color: #777; -} - -th.reorder-handle, -td.reorder-handle { - display: none; - width: 60px; - text-align: center; - cursor: move; -} - -.reorder-active { - th.reorder-handle, - td.reorder-handle { - display: table-cell; - } -} - -.reorder-placeholder th, -.reorder-placeholder td { - background-color: $warningBackground !important; -} - -.form-horizontal .ace_editor { - height: 96px; - border: 1px solid $inputBorder; - @include border-radius($inputBorderRadius); -} - -.control-group a[rel='tooltip'] { - margin-left: 8px; -} - -.qtip-bootstrap { - font-size: 12px; - max-width: 320px; - - pre { - font-size: 11px; - line-height: 15px; - } -} - -.qtip-wide { - max-width: 700px; -} - -.qtip-forced-wide { - min-width: 400px; - max-width: 600px; -} - -.qtip-content p:last-child { - margin-bottom: 0px; -} - -.query-syntax-help .example { - text-align: right; -} - -.query-syntax-help td.example { - font-style: italic; - font-size: 11px; -} - -.table-small { - font-size: 13px; -} - -.small-form { - input { - font-size: 12px; - padding: 2px 4px; - } - - .add-on { - font-size: 12px; - padding: 2px 5px; - } - - select { - font-size: 11px; - height: 26px; - line-height: 26px; - margin-bottom: 0px; - padding-top: 2px; - padding-bottom: 2px; - } -} - -.table tr.empty td { - text-align: center; - font-size: 12px; - font-style: italic; - color: #555; -} - -.table th.text-center, -.table td.text-center { - text-align: center; -} - -.table tr.line-bottom th, -.table tr.line-bottom td { - border-top: none; - border-bottom: 1px solid $tableBorder; -} - -.diff { - del { - text-decoration: none; - background: #fdd; - } - - ins { - text-decoration: none; - background: #dfd; - } -} - -.dropdown-menu { - .fa { - /* Make the icons a consistent width so the menu text lines up */ - width: 16px; - text-align: center; - } -} - -/* Pulled from Bootstrap 3. Remove once we upgrade */ -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: 12px; - line-height: 1.42857143; - color: #777; - white-space: nowrap; -} - -.publish-toggle-checkboxes { - margin: -28px 0px 32px 17px; - text-align: center; - width: 80px; - font-size: 85%; -} - -.control-group-negative-top { - margin-top: -16px; -} - -#version_footer .row-fluid { - border-top: 1px solid #ddd; - text-align: center; - margin-top: 3em; - margin-bottom: 1em; - padding-top: 1em; - font-size: 11px; - color: #777; -} diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css b/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css new file mode 100644 index 000000000..f6140e3a1 --- /dev/null +++ b/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css @@ -0,0 +1,93 @@ +/* + *= require vendor/normalize.css + */ + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body { + padding: 20px; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 16px; +} + +h1 { + margin: 0px; + text-align: center; + font-weight: 200; +} + +.btn { + display: block; + width: 100%; + text-decoration: none; + text-align: center; + cursor: pointer; + background-color: #07c; + color: #fff; + border-width: 0px; + padding: 10px; + border-radius: 5px; +} + +.btn:hover { + background-color: #005a9a; +} + +.btn:disabled, +.btn[disabled]:hover, +.btn[disabled]:focus { + cursor: not-allowed; + filter: alpha(opacity=65); + background-color: #ddd; + color: #666; +} + +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +#login, .alert { + max-width: 360px; + margin-left: auto; + margin-right: auto; +} + +.admin-login { + margin: 20px 0px; +} + +.admin-login form { + margin: 0px; +} + +.admin-login .alert { + margin-bottom: 2px; + font-size: 14px; +} diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin/stats.css.scss b/src/api-umbrella/web-app/app/assets/stylesheets/admin/stats.css.scss deleted file mode 100644 index 37f5f037d..000000000 --- a/src/api-umbrella/web-app/app/assets/stylesheets/admin/stats.css.scss +++ /dev/null @@ -1,140 +0,0 @@ -.toggle-facet-table { - font-size: 11px; -} - -.facet-table { - font-size: 11px; - - th, - td { - padding: 2px; - line-height: 14px; - } - - th.value, - td.value { - text-align: right; - } -} - -#map_container { - width: 640px; - margin: 0px auto 20px auto; -} - -.advanced-filter { - .input-append { - margin-bottom: 0px; - } - - input { - font-size: 12px; - padding: 2px 3px; - min-height: 20px; - } - - .help-block { - font-size: 85%; - line-height: 110%; - margin-top: 2px; - } -} - -.filter-times { - text-align: right; - - #interval_buttons { - display: inline-block; - margin: 4px 30px 0px 0px; - } - - #reportrange { - display: inline-block; - } -} - -.number-highlights { - margin: 20px 0px; -} - -.number-highlight { - text-align: center; - - strong { - font-size: 54px; - line-height: 100%; - font-weight: 100; - display: block; - - .units { - font-size: 28px; - line-height: 100%; - } - } - - small { - font-weight: bold; - font-size: 14px; - } -} - -#filter_toggle .fa { - width: 8px; -} - -#filters_ui { - max-width: 960px; - - .filter-type-toggle { - font-size: 12px; - text-align: right; - } - - #search_field { - .input-append { - display: block; - - .full-width-input, - .full-width-submit { - display: table-cell; - } - - .full-width-input { - width: 100%; - } - - .full-width-input input { - width: 100%; - box-sizing: border-box; - height: 26px; - } - } - } -} - -.query-builder { - .btn-xs { - font-size: 12px; - padding: 1px 5px; - } - - .rule-container input[type=number], - .rule-container input[type=text], - .rule-container select { - font-size: 11px; - height: 24px; - margin: 0px; - box-sizing: border-box; - padding: 1px 3px; - } - - .rule-container input[type=number] { - width: 102px; - } - - .rules-group-container { - background: #edf5fc; - background: rgba(237, 245, 252, 0.5); - border-color: #96bedc; - } -} diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin/variables.css.scss b/src/api-umbrella/web-app/app/assets/stylesheets/admin/variables.css.scss deleted file mode 100644 index 584c51db6..000000000 --- a/src/api-umbrella/web-app/app/assets/stylesheets/admin/variables.css.scss +++ /dev/null @@ -1,3 +0,0 @@ -$navbarInverseBackground: #262626; -$navbarInverseBackgroundHighlight: #3e3e3e; -$navbarInverseLinkColor: #bbb; diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin_test.css.scss b/src/api-umbrella/web-app/app/assets/stylesheets/admin_test.css.scss deleted file mode 100644 index 1e28f870a..000000000 --- a/src/api-umbrella/web-app/app/assets/stylesheets/admin_test.css.scss +++ /dev/null @@ -1,14 +0,0 @@ -// Attempt to disable animations in test mode to improve the -// reliability of some Capybara timing stuff without sleeping: -// http://stackoverflow.com/a/13119950 -// Note that we're disabling transitions completely, which doesn't work in -// Bootstrap unless we also define $.support.transition = false in -// admin_test.js (the suggestion from stackoverflow to lower the timeout still -// leads to some reproducible funkiness). -* { - -webkit-transition: none !important; - -moz-transition: none !important; - -ms-transition: none !important; - -o-transition: none !important; - transition: none !important; -} diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/application.css.scss b/src/api-umbrella/web-app/app/assets/stylesheets/application.css.scss deleted file mode 100644 index b7e72c103..000000000 --- a/src/api-umbrella/web-app/app/assets/stylesheets/application.css.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import "bootstrap"; -@import "bootstrap-responsive"; - -body { - padding-top: 60px; -} - -.container-fluid { - max-width: 1000px; - margin: 0px auto; -} - -.admin-logins { - form { - margin: 0px; - } - - .row-fluid { - margin-bottom: 15px; - } - - .alert { - margin-bottom: 2px; - } -} diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/vendor/normalize.css b/src/api-umbrella/web-app/app/assets/stylesheets/vendor/normalize.css new file mode 100644 index 000000000..9b77e0eb4 --- /dev/null +++ b/src/api-umbrella/web-app/app/assets/stylesheets/vendor/normalize.css @@ -0,0 +1,461 @@ +/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Change the default font family in all browsers (opinionated). + * 2. Correct the line height in all browsers. + * 3. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +/* Document + ========================================================================== */ + +html { + font-family: sans-serif; /* 1 */ + line-height: 1.15; /* 2 */ + -ms-text-size-adjust: 100%; /* 3 */ + -webkit-text-size-adjust: 100%; /* 3 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * Remove the outline on focused links when they are also active or hovered + * in all browsers (opinionated). + */ + +a:active, +a:hover { + outline-width: 0; +} + +/** + * 1. Remove the bottom border in Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], /* 1 */ +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Change the border, margin, and padding in all browsers (opinionated). + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, /* 1 */ +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb index 045736916..03a7177e7 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb @@ -1,5 +1,9 @@ class Admin::Admins::OmniauthCallbacksController < Devise::OmniauthCallbacksController - skip_after_filter :verify_authorized + skip_after_action :verify_authorized + + # The developer strategy doesn't include the CSRF token in the form: + # https://github.com/omniauth/omniauth/pull/674 + skip_before_action :verify_authenticity_token, :only => :developer # For the developer strategy, simply find or create a new admin account with # whatever login details they give. This is not for use on production. @@ -8,13 +12,11 @@ def developer raise "The developer OmniAuth strategy should not be used outside of development or test." end - omniauth = request.env["omniauth.auth"] - @admin = Admin.where(:username => omniauth["uid"]).first - @admin ||= Admin.new({ :username => omniauth["uid"], :superuser => true }, :without_protection => true) - @admin.apply_omniauth(omniauth) - @admin.save! - sign_in(:admin, @admin) - redirect_to admin_path + @email = request.env["omniauth.auth"]["uid"] + @admin = Admin.where(:username => @email).first + @admin ||= Admin.create!(:username => @email, :superuser => true) + + login end def cas @@ -46,11 +48,6 @@ def google_oauth2 login end - def myusa - @email = request.env["omniauth.auth"]["info"]["email"] - login - end - def persona @email = request.env["omniauth.auth"]["info"]["email"] login @@ -66,7 +63,7 @@ def ldap private def login - if @email.present? + if(!@admin && @email.present?) @admin = Admin.where(:username => @email.downcase).first end @@ -86,7 +83,13 @@ def login sign_in_and_redirect(:admin, @admin) else - flash[:error] = %(The account for '#{@email}' is not authorized to access the admin. Please
        contact us for further assistance.).html_safe + flash[:error] = ActionController::Base.helpers.safe_join([ + "The account for '", + @email, + "' is not authorized to access the admin. Please ", + ActionController::Base.helpers.content_tag(:a, "contact us", :href => ApiUmbrellaConfig[:contact_url]), + " for further assistance.", + ]) redirect_to new_admin_session_path end diff --git a/src/api-umbrella/web-app/app/controllers/admin/base_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/base_controller.rb index a381d9cfb..9359c0646 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/base_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/base_controller.rb @@ -1,18 +1,5 @@ class Admin::BaseController < ApplicationController - before_filter :authenticate_admin! - after_filter :verify_authorized - skip_after_filter :verify_authorized, :only => [:empty] - before_filter :set_locale - - layout "admin" - - def empty - render(:text => "", :layout => true) - end - - private - - def set_locale - I18n.locale = http_accept_language.compatible_language_from(I18n.available_locales) - end + before_action :authenticate_admin! + after_action :verify_authorized + skip_after_action :verify_authorized, :only => [:empty] end diff --git a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb index 99eb99314..9920f7fd3 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb @@ -1,6 +1,5 @@ class Admin::SessionsController < Devise::SessionsController - before_filter :set_locale - skip_after_filter :verify_authorized + skip_after_action :verify_authorized def new end @@ -9,9 +8,20 @@ def after_sign_out_path_for(resource_or_scope) admin_path end - private + def auth + response = { + "authenticated" => !current_admin.nil?, + "enable_beta_analytics" => (ApiUmbrellaConfig[:analytics][:adapter] == "kylin" || (ApiUmbrellaConfig[:analytics][:outputs] && ApiUmbrellaConfig[:analytics][:outputs].include?("kylin"))), + } - def set_locale - I18n.locale = http_accept_language.compatible_language_from(I18n.available_locales) + if current_admin + response["admin"] = current_admin.as_json + response["api_key"] = ApiUser.where(:email => "web.admin.ajax@internal.apiumbrella").order_by(:created_at.asc).first.api_key + response["csrf_token"] = form_authenticity_token if(protect_against_forgery?) + end + + respond_to do|format| + format.json { render(:json => response) } + end end end diff --git a/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb index 891e512d2..f149949a5 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb @@ -1,12 +1,10 @@ require "csv_streamer" class Admin::StatsController < Admin::BaseController - set_tab :analytics - - before_filter :set_analytics_adapter - around_filter :set_time_zone - skip_after_filter :verify_authorized - after_filter :verify_policy_scoped + before_action :set_analytics_adapter + around_action :set_time_zone + skip_after_action :verify_authorized + after_action :verify_policy_scoped def index end @@ -78,11 +76,11 @@ def logs format.csv do # Set Last-Modified so response streaming works: # http://stackoverflow.com/a/10252798/222487 - response.headers["Last-Modified"] = Time.now.httpdate + response.headers["Last-Modified"] = Time.now.utc.httpdate headers = ["Time", "Method", "Host", "URL", "User", "IP Address", "Country", "State", "City", "Status", "Reason Denied", "Response Time", "Content Type", "Accept Encoding", "User Agent"] - send_file_headers!(:disposition => "attachment", :filename => "api_logs (#{Time.now.strftime("%b %-e %Y")}).#{params[:format]}") + send_file_headers!(:disposition => "attachment", :filename => "api_logs (#{Time.now.utc.strftime("%b %-e %Y")}).#{params[:format]}") self.response_body = CsvStreamer.new(@result, headers) do |row| [ csv_time(row["request_at"]), @@ -181,7 +179,7 @@ def users :registration_source => user["registration_source"], :created_at => user["created_at"], :hits => bucket["doc_count"], - :last_request_at => Time.at(bucket["last_request_at"]["value"] / 1000), + :last_request_at => Time.at(bucket["last_request_at"]["value"] / 1000).utc, :use_description => user["use_description"], } end diff --git a/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb b/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb index cff82e318..36eb47c55 100644 --- a/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb @@ -5,8 +5,8 @@ def ip def logging @search = LogSearch.new({ - :start_time => Time.now - params[:age].to_i, - :end_time => Time.now, + :start_time => Time.now.utc - params[:age].to_i, + :end_time => Time.now.utc, }) @search.filter_by_date_range! diff --git a/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb index f07c07f80..466737787 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb @@ -1,7 +1,7 @@ class Api::V0::AnalyticsController < Api::V1::BaseController - before_filter :set_analytics_adapter - skip_before_filter :authenticate_admin!, :only => [:summary] - skip_after_filter :verify_authorized, :only => [:summary] + before_action :set_analytics_adapter + skip_before_action :authenticate_admin!, :only => [:summary] + skip_after_action :verify_authorized, :only => [:summary] def summary api_key_roles = request.headers['X-Api-Roles'].to_s.split(",") @@ -23,7 +23,7 @@ def summary # to generate, we want to err on the side of using the cache, so users # don't get a super slow response and we don't overwhelm the server when # it's uncached. - elsif(summary && summary[:cached_at] && summary[:cached_at] < Time.now - 6.hours) + elsif(summary && summary[:cached_at] && summary[:cached_at] < Time.now.utc - 6.hours) Thread.new do Rails.cache.write("analytics_summary", generate_summary, :expires_in => 2.days) end @@ -45,7 +45,7 @@ def generate_summary :hits_by_month => [], } - start_time = Time.parse("2013-07-01") + start_time = Time.parse("2013-07-01").utc # Fetch the user signups by month, trying to remove duplicate signups for # the same e-mail address (each e-mail address only gets counted for the first @@ -77,7 +77,7 @@ def generate_summary # Fill in missing months with 0 values. time = start_time - while(time < Time.now) + while(time < Time.now.utc) by_month["#{time.year}-#{time.month}"] ||= { :year => time.year, :month => time.month, @@ -97,7 +97,7 @@ def generate_summary # Fetch the hits by month. search = LogSearch.factory(@analytics_adapter, { :start_time => start_time, - :end_time => Time.now, + :end_time => Time.now.utc, :interval => "month", # This query can take a long time to run against PrestoDB, so set a long @@ -119,7 +119,7 @@ def generate_summary result = search.result result.hits_over_time.sort.each do |key, value| - time = Time.at(key / 1000) + time = Time.at(key / 1000).utc summary[:hits_by_month] << { :year => time.year, @@ -130,7 +130,7 @@ def generate_summary summary[:total_hits] += value end - summary[:cached_at] = Time.now + summary[:cached_at] = Time.now.utc summary end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/admin_groups_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/admin_groups_controller.rb index d035500ef..dc6a72759 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/admin_groups_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/admin_groups_controller.rb @@ -1,8 +1,8 @@ class Api::V1::AdminGroupsController < Api::V1::BaseController respond_to :json - skip_after_filter :verify_authorized, :only => [:index] - after_filter :verify_policy_scoped, :only => [:index] + skip_after_action :verify_authorized, :only => [:index] + after_action :verify_policy_scoped, :only => [:index] def index @admin_groups = policy_scope(AdminGroup).order_by(datatables_sort_array) @@ -57,8 +57,21 @@ def destroy def save! authorize(@admin_group) unless(@admin_group.new_record?) - @admin_group.assign_attributes(params[:admin_group], :as => :admin) + @admin_group.assign_attributes(admin_group_params) authorize(@admin_group) @admin_group.save end + + def admin_group_params + params.require(:admin_group).permit([ + :name, + { + :permission_ids => [], + :api_scope_ids => [], + }, + ]) + rescue => e + logger.error("Parameters error: #{e}") + ActionController::Parameters.new({}).permit! + end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/admin_permissions_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/admin_permissions_controller.rb index 3faf69acb..1bcfb6b64 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/admin_permissions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/admin_permissions_controller.rb @@ -1,5 +1,5 @@ class Api::V1::AdminPermissionsController < Api::V1::BaseController - skip_after_filter :verify_authorized, :only => [:index] + skip_after_action :verify_authorized, :only => [:index] def index @admin_permissions = AdminPermission.sorted.all.to_a diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb index 5367d2df5..092e1d42d 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb @@ -1,8 +1,8 @@ class Api::V1::AdminsController < Api::V1::BaseController respond_to :json - skip_after_filter :verify_authorized, :only => [:index] - after_filter :verify_policy_scoped, :only => [:index] + skip_after_action :verify_authorized, :only => [:index] + after_action :verify_policy_scoped, :only => [:index] def index @admins = policy_scope(Admin).order_by(datatables_sort_array) @@ -28,7 +28,7 @@ def index @admins_count = @admins.count @admins = @admins.to_a.select { |admin| Pundit.policy!(pundit_user, admin).show? } - self.respond_to_datatables(@admins, "admins #{Time.now.strftime("%b %-e %Y")}") + self.respond_to_datatables(@admins, "admins #{Time.now.utc.strftime("%b %-e %Y")}") end def show @@ -73,8 +73,22 @@ def destroy def save! authorize(@admin) unless(@admin.new_record?) - @admin.assign_attributes(params[:admin], :as => :admin) + @admin.assign_attributes(admin_params) authorize(@admin) @admin.save end + + def admin_params + params.require(:admin).permit([ + :username, + :email, + :name, + :notes, + :superuser, + { :group_ids => [] }, + ]) + rescue => e + logger.error("Parameters error: #{e}") + ActionController::Parameters.new({}).permit! + end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/analytics_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/analytics_controller.rb index f23460048..254c3d115 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/analytics_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/analytics_controller.rb @@ -1,10 +1,10 @@ class Api::V1::AnalyticsController < Api::V1::BaseController include ActionView::Helpers::NumberHelper - before_filter :set_analytics_adapter - skip_after_filter :verify_authorized - after_filter :verify_policy_scoped - around_filter :set_time_zone + before_action :set_analytics_adapter + skip_after_action :verify_authorized + after_action :verify_policy_scoped + around_action :set_time_zone def drilldown @search = LogSearch.factory(@analytics_adapter, { @@ -40,7 +40,7 @@ def drilldown parents.each_with_index do |parent, index| @breadcrumbs << { :crumb => parent, - :prefix => File.join((index + 1).to_s, parents[0..index].join("/"), "/") + :prefix => File.join((index + 1).to_s, parents[0..index].join("/"), "/"), } end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/api_scopes_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/api_scopes_controller.rb index 1028adb50..4cbb26080 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/api_scopes_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/api_scopes_controller.rb @@ -1,8 +1,8 @@ class Api::V1::ApiScopesController < Api::V1::BaseController respond_to :json - skip_after_filter :verify_authorized, :only => [:index] - after_filter :verify_policy_scoped, :only => [:index] + skip_after_action :verify_authorized, :only => [:index] + after_action :verify_policy_scoped, :only => [:index] def index @api_scopes = policy_scope(ApiScope).order_by(datatables_sort_array) @@ -56,8 +56,19 @@ def destroy def save! authorize(@api_scope) unless(@api_scope.new_record?) - @api_scope.assign_attributes(params[:api_scope], :as => :admin) + @api_scope.assign_attributes(api_scope_params) authorize(@api_scope) @api_scope.save end + + def api_scope_params + params.require(:api_scope).permit([ + :name, + :host, + :path_prefix, + ]) + rescue => e + logger.error("Parameters error: #{e}") + ActionController::Parameters.new({}).permit! + end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/apis_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/apis_controller.rb index 86d3d72ed..94411410c 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/apis_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/apis_controller.rb @@ -1,8 +1,8 @@ class Api::V1::ApisController < Api::V1::BaseController respond_to :json - skip_after_filter :verify_authorized, :only => [:index] - after_filter :verify_policy_scoped, :only => [:index] + skip_after_action :verify_authorized, :only => [:index] + after_action :verify_policy_scoped, :only => [:index] def index @apis = policy_scope(Api).order_by(datatables_sort_array) @@ -78,9 +78,112 @@ def move_after def save! authorize(@api) unless(@api.new_record?) - @api.assign_nested_attributes(params[:api], :as => :admin) + @api.assign_nested_attributes(api_params) authorize(@api) @api.save end + + def api_params + settings_permitted = [ + :id, + :append_query_string, + :http_basic_auth, + :require_https, + :require_https_transition_start_at, + :disable_api_key, + :api_key_verification_level, + :api_key_verification_transition_start_at, + :rate_limit_mode, + :anonymous_rate_limit_behavior, + :authenticated_rate_limit_behavior, + :pass_api_key_header, + :pass_api_key_query_param, + :required_roles_override, + :headers_string, + :default_response_headers_string, + :override_response_headers_string, + { + :required_roles => [], + :allowed_ips => [], + :allowed_referers => [], + :error_templates => [ + :json, + :xml, + :csv, + ], + :default_response_headers => [ + :id, + :key, + :value, + ], + :override_response_headers => [ + :id, + :key, + :value, + ], + :headers => [ + :id, + :key, + :value, + ], + :error_data_yaml_strings => [ + :common, + :api_key_missing, + :api_key_invalid, + :api_key_disabled, + :api_key_unauthorized, + :over_rate_limit, + :https_required, + ], + :rate_limits => [ + :id, + :duration, + :limit_by, + :limit, + :response_headers, + ], + }, + ] + + params.require(:api).permit([ + :name, + :sort_order, + :backend_protocol, + :frontend_host, + :backend_host, + :balance_algorithm, + { + :settings => settings_permitted, + :servers => [ + :id, + :host, + :port, + ], + :url_matches => [ + :id, + :frontend_prefix, + :backend_prefix, + ], + :sub_settings => [ + :id, + :http_method, + :regex, + { + :settings => settings_permitted, + }, + ], + :rewrites => [ + :id, + :matcher_type, + :http_method, + :frontend_matcher, + :backend_replacement, + ], + }, + ]) + rescue => e + logger.error("Parameters error: #{e}") + ActionController::Parameters.new({}).permit! + end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb index cb48ff133..68cd6e9d4 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb @@ -1,12 +1,12 @@ class Api::V1::BaseController < ApplicationController # Try authenticating from an admin token (for direct API access). - before_filter :authenticate_admin_from_token! + before_action :authenticate_admin_from_token! # If no admin token is present, authenticate normally with Devise from a # session (so this API can be used from within the web admin tool). - before_filter :authenticate_admin! + before_action :authenticate_admin! - after_filter :verify_authorized + after_action :verify_authorized rescue_from Pundit::NotAuthorizedError, :with => :user_not_authorized @@ -21,13 +21,12 @@ def authenticate_admin_from_token! # request. sign_in(admin, :store => false) - # The mongoid_userstamp plugin doesn't seem to pickup the current admin - # user when we load via this token (something to do with callback - # ordering?). To to fix that, force the userstamp model to pickup the - # current admin account after this token-based login. - unless Mongoid::Userstamp.current_user + # The normal userstamp before_action that set's the current admin fires + # before we handle token authentication. To fix that, force the userstamp + # model to pickup the current admin account after this token-based login. + unless RequestStore.store[:current_userstamp_user] begin - Mongoid::Userstamp.config.user_model.current = self.send(Mongoid::Userstamp.config.user_reader) + RequestStore.store[:current_userstamp_user] = current_admin rescue => e Rails.logger.warn("Unexpected error setting userstamp: #{e}") end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/config_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/config_controller.rb index 005fe30fd..f44b1bc71 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/config_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/config_controller.rb @@ -1,7 +1,7 @@ class Api::V1::ConfigController < Api::V1::BaseController respond_to :json - skip_after_filter :verify_authorized, :only => [:pending_changes] + skip_after_action :verify_authorized, :only => [:pending_changes] def pending_changes @changes = ConfigVersion.pending_changes(pundit_user) diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/contacts_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/contacts_controller.rb index cdc222a98..a81ca9f4a 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/contacts_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/contacts_controller.rb @@ -1,14 +1,14 @@ class Api::V1::ContactsController < Api::V1::BaseController - skip_before_filter :authenticate_admin!, :only => [:create] - before_filter :authenicate_contact_api_key_role, :only => [:create] - skip_after_filter :verify_authorized + skip_before_action :authenticate_admin!, :only => [:create] + before_action :authenicate_contact_api_key_role, :only => [:create] + skip_after_action :verify_authorized def create - @contact = Contact.new(params[:contact]) + @contact = Contact.new(contact_params) respond_to do |format| if(@contact.deliver) - format.json { render(:json => { :submitted => Time.now }, :status => :ok) } + format.json { render(:json => { :submitted => Time.now.utc }, :status => :ok) } else format.json { render(:json => errors_response(@contact), :status => :unprocessable_entity) } end @@ -29,4 +29,17 @@ def authenicate_contact_api_key_role end end end + + def contact_params + params.require(:contact).permit([ + :name, + :email, + :api, + :subject, + :message, + ]) + rescue => e + logger.error("Parameters error: #{e}") + ActionController::Parameters.new({}).permit! + end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/user_roles_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/user_roles_controller.rb index 15df742db..af13e2b32 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/user_roles_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/user_roles_controller.rb @@ -1,5 +1,5 @@ class Api::V1::UserRolesController < Api::V1::BaseController - skip_after_filter :verify_authorized, :only => [:index] + skip_after_action :verify_authorized, :only => [:index] def index @roles = ApiUserRole.all diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/users_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/users_controller.rb index 5c4f7b8fa..2a09df40e 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/users_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/users_controller.rb @@ -1,11 +1,11 @@ class Api::V1::UsersController < Api::V1::BaseController respond_to :json - skip_before_filter :authenticate_admin!, :only => [:create] - before_filter :authenicate_creator_api_key_role, :only => [:create] - skip_after_filter :verify_authorized, :only => [:index, :create] - after_filter :verify_policy_scoped, :only => [:index] - before_filter :parse_post_for_pseudo_ie_cors, :only => [:create] + skip_before_action :authenticate_admin!, :only => [:create] + before_action :authenicate_creator_api_key_role, :only => [:create] + skip_after_action :verify_authorized, :only => [:index, :create] + after_action :verify_policy_scoped, :only => [:index] + before_action :parse_post_for_pseudo_ie_cors, :only => [:create] def index @api_users = policy_scope(ApiUser).order_by(datatables_sort_array) @@ -66,10 +66,10 @@ def create end if(send_welcome_email) - ApiUserMailer.delay(:queue => "mailers").signup_email(@api_user, params[:options] || {}) + ApiUserMailer.signup_email(@api_user.id, params[:options] || {}).deliver_later end if(send_notify_email) - ApiUserMailer.delay(:queue => "mailers").notify_api_admin(@api_user) + ApiUserMailer.notify_api_admin(@api_user.id).deliver_later end format.json { render("show", :status => :created, :location => api_v1_user_url(@api_user)) } @@ -96,13 +96,7 @@ def update def assign_attributes! authorize(@api_user) unless(@api_user.new_record?) - - assign_options = {} - if(admin_signed_in?) - assign_options[:as] = :admin - end - - @api_user.assign_nested_attributes(params[:user], assign_options) + @api_user.assign_nested_attributes(user_params) @verify_email = (params[:options] && params[:options][:verify_email].to_s == "true") if(@verify_email || admin_signed_in?) @@ -134,4 +128,48 @@ def authenicate_creator_api_key_role end end end + + def user_params + attrs = [ + :first_name, + :last_name, + :email, + :website, + :use_description, + :terms_and_conditions, + :registration_source, + ] + + if(admin_signed_in?) + attrs += [ + :throttle_by_ip, + :enabled, + { + :roles => [], + :settings => [ + :id, + :rate_limit_mode, + :allowed_ips, + :allowed_referers, + { + :allowed_ips => [], + :allowed_referers => [], + :rate_limits => [ + :id, + :duration, + :limit_by, + :limit, + :response_headers, + ], + }, + ], + }, + ] + end + + params.require(:user).permit(attrs) + rescue => e + logger.error("Parameters error: #{e}") + ActionController::Parameters.new({}).permit! + end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/website_backends_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/website_backends_controller.rb index 313bcec4c..e667a9fb7 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/website_backends_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/website_backends_controller.rb @@ -1,8 +1,8 @@ class Api::V1::WebsiteBackendsController < Api::V1::BaseController respond_to :json - skip_after_filter :verify_authorized, :only => [:index] - after_filter :verify_policy_scoped, :only => [:index] + skip_after_action :verify_authorized, :only => [:index] + after_action :verify_policy_scoped, :only => [:index] def index @website_backends = policy_scope(WebsiteBackend).order_by(datatables_sort_array) @@ -55,8 +55,20 @@ def destroy def save! authorize(@website_backend) unless(@website_backend.new_record?) - @website_backend.assign_attributes(params[:website_backend], :as => :admin) + @website_backend.assign_attributes(website_backend_params) authorize(@website_backend) @website_backend.save end + + def website_backend_params + params.require(:website_backend).permit([ + :frontend_host, + :backend_protocol, + :server_host, + :server_port, + ]) + rescue => e + logger.error("Parameters error: #{e}") + ActionController::Parameters.new({}).permit! + end end diff --git a/src/api-umbrella/web-app/app/controllers/application_controller.rb b/src/api-umbrella/web-app/app/controllers/application_controller.rb index 82b841787..ce205770e 100644 --- a/src/api-umbrella/web-app/app/controllers/application_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/application_controller.rb @@ -1,7 +1,18 @@ class ApplicationController < ActionController::Base include Pundit include DatatablesHelper - protect_from_forgery + prepend_around_filter :use_locale + protect_from_forgery :with => :null_session + + around_action :set_userstamp + + def after_sign_in_path_for(resource) + if(resource.is_a?(Admin)) + "/admin/#/login" + else + super + end + end def pundit_user current_admin @@ -40,9 +51,9 @@ def csv_time(time) if(time) case(time) when String - time = Time.parse(time) + time = Time.parse(time).utc when Numeric - time = Time.at(time / 1000.0) + time = Time.at(time / 1000.0).utc end time.utc.strftime("%Y-%m-%d %H:%M:%S") @@ -71,6 +82,13 @@ def parse_post_for_pseudo_ie_cors end end + def use_locale + locale = http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale + I18n.with_locale(locale) do + yield + end + end + def set_analytics_adapter if(params[:beta_analytics] == "true") @analytics_adapter = "kylin" @@ -86,4 +104,14 @@ def set_time_zone ensure Time.zone = old_time_zone end + + private + + def set_userstamp + orig = RequestStore.store[:current_userstamp_user] + RequestStore.store[:current_userstamp_user] = current_admin + yield + ensure + RequestStore.store[:current_userstamp_user] = orig + end end diff --git a/src/api-umbrella/web-app/app/helpers/admin/stats_helper.rb b/src/api-umbrella/web-app/app/helpers/admin/stats_helper.rb index b1d1453d2..87c0ec7e3 100644 --- a/src/api-umbrella/web-app/app/helpers/admin/stats_helper.rb +++ b/src/api-umbrella/web-app/app/helpers/admin/stats_helper.rb @@ -79,12 +79,12 @@ def region_name(code) name = code case(params[:region]) when "world" - country = Country[code] + country = ISO3166::Country.new(code) if country name = country.name end when /^[A-Z]{2}$/ - country = Country[params[:region]] + country = ISO3166::Country.new(params[:region]) if country state = country.states[code] if(state) diff --git a/src/api-umbrella/web-app/app/helpers/application_helper.rb b/src/api-umbrella/web-app/app/helpers/application_helper.rb index 36a7d85f4..19f895b79 100644 --- a/src/api-umbrella/web-app/app/helpers/application_helper.rb +++ b/src/api-umbrella/web-app/app/helpers/application_helper.rb @@ -1,4 +1,19 @@ module ApplicationHelper + def bootstrap_class_for(flash_type) + case flash_type.to_s + when "success" + "alert-success" + when "error" + "alert-danger" + when "alert" + "alert-warning" + when "notice" + "alert-info" + else + "alert-#{flash_type}" + end + end + def web_admin_ajax_api_user user = ApiUser.where(:email => "web.admin.ajax@internal.apiumbrella").order_by(:created_at.asc).first unless(user) diff --git a/src/api-umbrella/web-app/app/helpers/datatables_helper.rb b/src/api-umbrella/web-app/app/helpers/datatables_helper.rb index d146a39e0..a5bc26f95 100644 --- a/src/api-umbrella/web-app/app/helpers/datatables_helper.rb +++ b/src/api-umbrella/web-app/app/helpers/datatables_helper.rb @@ -41,8 +41,9 @@ def datatables_columns columns = self.param_index_array(:columns) columns = columns.select { |col| col[:data] } columns.map do |col| - { :name => (col[:name] || '-').to_s, - :field => col[:data].to_s + { + :name => (col[:name] || '-').to_s, + :field => col[:data].to_s, } end end diff --git a/src/api-umbrella/web-app/app/mailers/api_user_mailer.rb b/src/api-umbrella/web-app/app/mailers/api_user_mailer.rb index ba80d4f69..e37bc723a 100644 --- a/src/api-umbrella/web-app/app/mailers/api_user_mailer.rb +++ b/src/api-umbrella/web-app/app/mailers/api_user_mailer.rb @@ -1,10 +1,8 @@ -require "mail_sanitizer" - class ApiUserMailer < ActionMailer::Base default :from => "noreply@#{ApiUmbrellaConfig[:web][:default_host]}" - def signup_email(user, options) - @user = user + def signup_email(user_id, options) + @user = ApiUser.find(user_id) if(options[:example_api_url].present?) @example_api_url = options[:example_api_url].gsub("{{api_key}}", @user.api_key) @@ -20,19 +18,19 @@ def signup_email(user, options) end mail :subject => "Your #{site_name} API key", - :from => MailSanitizer.sanitize_address(from), - :to => MailSanitizer.sanitize_address(user.email) + :from => from, + :to => @user.email end - def notify_api_admin(user) - @user = user + def notify_api_admin(user_id) + @user = ApiUser.find(user_id) to = ApiUmbrellaConfig[:web][:admin_notify_email].presence || ApiUmbrellaConfig[:web][:contact_form_email] full_name = "#{@user.first_name} #{@user.last_name}" from = "noreply@#{ApiUmbrellaConfig[:web][:default_host]}" mail :subject => "#{full_name} just subscribed", - :from => MailSanitizer.sanitize_address(from), - :to => MailSanitizer.sanitize_address(to) + :from => from, + :to => to end end diff --git a/src/api-umbrella/web-app/app/mailers/contact_mailer.rb b/src/api-umbrella/web-app/app/mailers/contact_mailer.rb index e61ed39f9..a59a8cbde 100644 --- a/src/api-umbrella/web-app/app/mailers/contact_mailer.rb +++ b/src/api-umbrella/web-app/app/mailers/contact_mailer.rb @@ -1,13 +1,11 @@ -require "mail_sanitizer" - class ContactMailer < ActionMailer::Base default :from => "noreply@#{ApiUmbrellaConfig[:web][:default_host]}" def contact_email(contact) @contact = contact - mail :reply_to => MailSanitizer.sanitize_address(contact.email), + mail :reply_to => contact.email, :subject => "#{ApiUmbrellaConfig[:site_name]} Contact Message from #{contact.email}", - :to => MailSanitizer.sanitize_address(ApiUmbrellaConfig[:web][:contact_form_email]) + :to => ApiUmbrellaConfig[:web][:contact_form_email] end end diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index 4098d22b2..91ca7e6c0 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -9,7 +9,7 @@ class Admin devise :omniauthable, :trackable # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :username, :type => String field :email, :type => String field :name, :type => String @@ -42,15 +42,6 @@ class Admin # Callbacks before_validation :generate_authentication_token, :on => :create - # Mass assignment security - attr_accessible :username, - :email, - :name, - :notes, - :superuser, - :group_ids, - :as => [:admin] - def self.sorted order_by(:username.asc) end diff --git a/src/api-umbrella/web-app/app/models/admin_group.rb b/src/api-umbrella/web-app/app/models/admin_group.rb index b4448ab5d..1c18cf3d6 100644 --- a/src/api-umbrella/web-app/app/models/admin_group.rb +++ b/src/api-umbrella/web-app/app/models/admin_group.rb @@ -6,7 +6,7 @@ class AdminGroup include Mongoid::Delorean::Trackable # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :name, :type => String # Relations @@ -22,12 +22,6 @@ class AdminGroup :presence => true validate :validate_permissions - # Mass assignment security - attr_accessible :name, - :permission_ids, - :api_scope_ids, - :as => [:admin] - def self.sorted order_by(:name.asc) end diff --git a/src/api-umbrella/web-app/app/models/admin_permission.rb b/src/api-umbrella/web-app/app/models/admin_permission.rb index 6956bb58e..fe4a47c82 100644 --- a/src/api-umbrella/web-app/app/models/admin_permission.rb +++ b/src/api-umbrella/web-app/app/models/admin_permission.rb @@ -5,7 +5,7 @@ class AdminPermission include Mongoid::Userstamp include Mongoid::Delorean::Trackable - field :_id, :type => String + field :_id, :type => String, :overwrite => true field :name, :type => String field :display_order, :type => Integer diff --git a/src/api-umbrella/web-app/app/models/api.rb b/src/api-umbrella/web-app/app/models/api.rb index ac6bfd84f..47bf78d43 100644 --- a/src/api-umbrella/web-app/app/models/api.rb +++ b/src/api-umbrella/web-app/app/models/api.rb @@ -15,7 +15,7 @@ class Api SORT_ORDER_GAP = 10_000 # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :name, :type => String field :sort_order, :type => Integer field :backend_protocol, :type => String @@ -66,20 +66,6 @@ class Api accepts_nested_attributes_for :settings accepts_nested_attributes_for :servers, :url_matches, :sub_settings, :rewrites, :allow_destroy => true - # Mass assignment security - attr_accessible :name, - :sort_order, - :backend_protocol, - :frontend_host, - :backend_host, - :balance_algorithm, - :settings_attributes, - :servers_attributes, - :url_matches_attributes, - :sub_settings_attributes, - :rewrites_attributes, - :as => [:default, :admin] - def self.sorted order_by(:sort_order.asc) end @@ -100,11 +86,11 @@ def as_json(options = {}) end root["creator"] = { - "username" => (self.creator.username if(self.creator)) + "username" => (self.creator.username if(self.creator)), } root["updater"] = { - "username" => (self.updater.username if(self.updater)) + "username" => (self.updater.username if(self.updater)), } json diff --git a/src/api-umbrella/web-app/app/models/api/header.rb b/src/api-umbrella/web-app/app/models/api/header.rb index 7954757f0..b01ccf54b 100644 --- a/src/api-umbrella/web-app/app/models/api/header.rb +++ b/src/api-umbrella/web-app/app/models/api/header.rb @@ -2,7 +2,7 @@ class Api::Header include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :key, :type => String field :value, :type => String @@ -13,11 +13,6 @@ class Api::Header validates :key, :presence => true - # Mass assignment security - attr_accessible :key, - :value, - :as => [:default, :admin] - def to_s "#{key}: #{value}" end diff --git a/src/api-umbrella/web-app/app/models/api/rate_limit.rb b/src/api-umbrella/web-app/app/models/api/rate_limit.rb index d2cd52eb4..efb51b334 100644 --- a/src/api-umbrella/web-app/app/models/api/rate_limit.rb +++ b/src/api-umbrella/web-app/app/models/api/rate_limit.rb @@ -2,7 +2,7 @@ class Api::RateLimit include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :duration, :type => Integer field :accuracy, :type => Integer field :limit_by, :type => String @@ -34,13 +34,6 @@ class Api::RateLimit before_validation :auto_calculate_accuracy before_validation :auto_calculate_distributed - # Mass assignment security - attr_accessible :duration, - :limit_by, - :limit, - :response_headers, - :as => [:default, :admin] - private def auto_calculate_accuracy diff --git a/src/api-umbrella/web-app/app/models/api/rewrite.rb b/src/api-umbrella/web-app/app/models/api/rewrite.rb index 33e74f61e..41b4af97a 100644 --- a/src/api-umbrella/web-app/app/models/api/rewrite.rb +++ b/src/api-umbrella/web-app/app/models/api/rewrite.rb @@ -2,7 +2,7 @@ class Api::Rewrite include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :matcher_type, :type => String field :http_method, :type => String field :frontend_matcher, :type => String @@ -16,11 +16,4 @@ class Api::Rewrite :inclusion => { :in => %w(route regex) } validates :http_method, :inclusion => { :in => %w(any GET POST PUT DELETE HEAD TRACE OPTIONS CONNECT PATCH) } - - # Mass assignment security - attr_accessible :matcher_type, - :http_method, - :frontend_matcher, - :backend_replacement, - :as => [:default, :admin] end diff --git a/src/api-umbrella/web-app/app/models/api/route.rb b/src/api-umbrella/web-app/app/models/api/route.rb index 4a47ea9fc..9b642a441 100644 --- a/src/api-umbrella/web-app/app/models/api/route.rb +++ b/src/api-umbrella/web-app/app/models/api/route.rb @@ -2,7 +2,7 @@ class Api::Route include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :matcher, :type => String field :http_method, :type => String field :from, :type => String diff --git a/src/api-umbrella/web-app/app/models/api/server.rb b/src/api-umbrella/web-app/app/models/api/server.rb index 74fb4c564..78b45f689 100644 --- a/src/api-umbrella/web-app/app/models/api/server.rb +++ b/src/api-umbrella/web-app/app/models/api/server.rb @@ -5,7 +5,7 @@ class Api::Server include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :host, :type => String field :port, :type => Integer @@ -24,11 +24,6 @@ class Api::Server :inclusion => { :in => 0..65_535 } validate :validate_host_resolves, :on => :create - # Mass assignment security - attr_accessible :host, - :port, - :as => [:default, :admin] - private def validate_host_resolves diff --git a/src/api-umbrella/web-app/app/models/api/settings.rb b/src/api-umbrella/web-app/app/models/api/settings.rb index 884b03969..1546fd2c4 100644 --- a/src/api-umbrella/web-app/app/models/api/settings.rb +++ b/src/api-umbrella/web-app/app/models/api/settings.rb @@ -2,7 +2,7 @@ class Api::Settings include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :append_query_string, :type => String field :http_basic_auth, :type => String field :require_https, :type => String @@ -48,34 +48,6 @@ class Api::Settings # Nested attributes accepts_nested_attributes_for :headers, :rate_limits, :default_response_headers, :override_response_headers, :allow_destroy => true - # Mass assignment security - attr_accessible :append_query_string, - :http_basic_auth, - :require_https, - :require_https_transition_start_at, - :disable_api_key, - :api_key_verification_level, - :api_key_verification_transition_start_at, - :rate_limit_mode, - :anonymous_rate_limit_behavior, - :authenticated_rate_limit_behavior, - :pass_api_key_header, - :pass_api_key_query_param, - :required_roles, - :required_roles_override, - :allowed_ips, - :allowed_referers, - :error_templates, - :error_data_yaml_strings, - :headers, - :headers_string, - :rate_limits_attributes, - :default_response_headers, - :default_response_headers_string, - :override_response_headers, - :override_response_headers_string, - :as => [:default, :admin] - def headers_string read_headers_string(:headers) end @@ -111,7 +83,7 @@ def error_data_yaml_strings @error_data_yaml_strings = {} if self.error_data.present? self.error_data.each do |key, value| - @error_data_yaml_strings[key] = Psych.dump(value).gsub(/^---\s*\n/, "").strip + @error_data_yaml_strings[key] = Psych.dump(value).gsub(/\A---.*?\n/, "").strip end end end @@ -143,7 +115,7 @@ def error_data_yaml_strings=(strings) def set_transition_starts_on_publish if(self.require_https =~ /^transition_/) if(self.require_https_transition_start_at.blank?) - self.require_https_transition_start_at = Time.now + self.require_https_transition_start_at = Time.now.utc end else if(self.require_https_transition_start_at.present?) @@ -153,7 +125,7 @@ def set_transition_starts_on_publish if(self.api_key_verification_level =~ /^transition_/) if(self.api_key_verification_transition_start_at.blank?) - self.api_key_verification_transition_start_at = Time.now + self.api_key_verification_transition_start_at = Time.now.utc end else if(self.api_key_verification_transition_start_at.present?) diff --git a/src/api-umbrella/web-app/app/models/api/sub_settings.rb b/src/api-umbrella/web-app/app/models/api/sub_settings.rb index 5bb3cad11..f31374d1f 100644 --- a/src/api-umbrella/web-app/app/models/api/sub_settings.rb +++ b/src/api-umbrella/web-app/app/models/api/sub_settings.rb @@ -2,7 +2,7 @@ class Api::SubSettings include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :http_method, :type => String field :regex, :type => String @@ -16,10 +16,4 @@ class Api::SubSettings # Nested attributes accepts_nested_attributes_for :settings - - # Mass assignment security - attr_accessible :http_method, - :regex, - :settings_attributes, - :as => [:default, :admin] end diff --git a/src/api-umbrella/web-app/app/models/api/url_match.rb b/src/api-umbrella/web-app/app/models/api/url_match.rb index 7aedd5f06..f3da48f7d 100644 --- a/src/api-umbrella/web-app/app/models/api/url_match.rb +++ b/src/api-umbrella/web-app/app/models/api/url_match.rb @@ -4,7 +4,7 @@ class Api::UrlMatch include Mongoid::Document # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :frontend_prefix, :type => String field :backend_prefix, :type => String @@ -24,9 +24,4 @@ class Api::UrlMatch :with => CommonValidations::URL_PREFIX_FORMAT, :message => :invalid_url_prefix_format, } - - # Mass assignment security - attr_accessible :frontend_prefix, - :backend_prefix, - :as => [:default, :admin] end diff --git a/src/api-umbrella/web-app/app/models/api_scope.rb b/src/api-umbrella/web-app/app/models/api_scope.rb index ce731a977..04fd6a296 100644 --- a/src/api-umbrella/web-app/app/models/api_scope.rb +++ b/src/api-umbrella/web-app/app/models/api_scope.rb @@ -8,7 +8,7 @@ class ApiScope include Mongoid::Delorean::Trackable # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :name, :type => String field :host, :type => String field :path_prefix, :type => String @@ -32,12 +32,6 @@ class ApiScope :scope => :host, } - # Mass assignment security - attr_accessible :name, - :host, - :path_prefix, - :as => [:admin] - def path_prefix_matcher /^#{Regexp.escape(self.path_prefix)}/ end diff --git a/src/api-umbrella/web-app/app/models/api_user.rb b/src/api-umbrella/web-app/app/models/api_user.rb index 25ffffddb..ebd3f0ad0 100644 --- a/src/api-umbrella/web-app/app/models/api_user.rb +++ b/src/api-umbrella/web-app/app/models/api_user.rb @@ -10,7 +10,7 @@ class ApiUser include ApiUmbrella::AttributifyData # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :api_key field :first_name field :last_name @@ -85,21 +85,6 @@ class ApiUser # Nested attributes accepts_nested_attributes_for :settings - # Mass assignment security - attr_accessible :first_name, - :last_name, - :email, - :website, - :use_description, - :terms_and_conditions, - :registration_source, - :as => [:default, :admin] - attr_accessible :roles, - :throttle_by_ip, - :enabled, - :settings_attributes, - :as => :admin - def self.human_attribute_name(attribute, options = {}) case(attribute.to_sym) when :email @@ -130,7 +115,7 @@ def enabled def enabled=(enabled) if(enabled.to_s == "false") if(self.disabled_at.nil?) - self.disabled_at = Time.now + self.disabled_at = Time.now.utc end else self.disabled_at = nil @@ -198,7 +183,7 @@ def handle_rate_limit_mode # that all of the records get touched with consistent server-side # timestamps). def touch_server_side_timestamp - collection.find({ :_id => self.id }).update({ + collection.update_one({ :_id => self.id }, { "$currentDate" => { "ts" => { "$type" => "timestamp" }, }, diff --git a/src/api-umbrella/web-app/app/models/config_version.rb b/src/api-umbrella/web-app/app/models/config_version.rb index 2b389c37f..eebe3dc5e 100644 --- a/src/api-umbrella/web-app/app/models/config_version.rb +++ b/src/api-umbrella/web-app/app/models/config_version.rb @@ -13,7 +13,7 @@ class ConfigVersion def self.publish!(config) self.create!({ - :version => Time.now, + :version => Time.now.utc, :config => config, }) end @@ -49,7 +49,7 @@ def self.active_config def self.pending_config { - "apis" => Api.sorted.all.map { |api| Hash[api.attributes] } + "apis" => Api.sorted.all.map { |api| Hash[api.attributes] }, } end @@ -153,7 +153,7 @@ def self.record_for_comparison(object) # data to import, since these are likely to differ even if the data is # really the same (since these depend on when the import was actually # performed). - duplicate.except!(*%w(version created_by created_at updated_at updated_by _id)) + duplicate.except!("version", "created_by", "created_at", "deleted_at", "updated_at", "updated_by", "_id") duplicate.each do |key, value| duplicate[key] = record_for_comparison(value) @@ -187,7 +187,7 @@ def self.stringify_object_ids(object) if(duplicate.kind_of?(Hash)) duplicate.each do |key, value| - if(value.kind_of?(Moped::BSON::ObjectId)) + if(value.kind_of?(BSON::ObjectId)) duplicate[key] = value.to_s else duplicate[key] = stringify_object_ids(value) @@ -204,7 +204,7 @@ def self.stringify_object_ids(object) def self.sort_hash_by_keys(object) if(object.kind_of?(Hash)) - object.keys.sort { |x, y| x.to_s <=> y.to_s }.reduce({}) do |sorted, key| + object.keys.sort_by(&:to_s).reduce({}) do |sorted, key| sorted[key] = sort_hash_by_keys(object[key]) sorted end diff --git a/src/api-umbrella/web-app/app/models/contact.rb b/src/api-umbrella/web-app/app/models/contact.rb index 63067c234..74919469f 100644 --- a/src/api-umbrella/web-app/app/models/contact.rb +++ b/src/api-umbrella/web-app/app/models/contact.rb @@ -1,12 +1,8 @@ class Contact - include ActiveModel::Conversion - include ActiveModel::MassAssignmentSecurity - include ActiveModel::Validations + include ActiveModel::Model attr_accessor :name, :email, :api, :subject, :message - attr_accessible :name, :email, :api, :subject, :message - validates :name, :presence => { :message => "Provide your first name." } validates :email, @@ -23,25 +19,11 @@ class Contact validates :message, :presence => { :message => "Provide a message." } - def initialize(attrs = {}) - self.attributes = attrs - end - - def attributes=(values) - sanitize_for_mass_assignment(values).each do |k, v| - __send__("#{k}=", v) - end - end - def deliver if self.valid? - ContactMailer.delay(:queue => "mailers").contact_email(self) + ContactMailer.contact_email(self).deliver_later else false end end - - def persisted? - false - end end diff --git a/src/api-umbrella/web-app/app/models/log_city_location.rb b/src/api-umbrella/web-app/app/models/log_city_location.rb index 4ab6570a8..7b4812644 100644 --- a/src/api-umbrella/web-app/app/models/log_city_location.rb +++ b/src/api-umbrella/web-app/app/models/log_city_location.rb @@ -2,7 +2,7 @@ class LogCityLocation include Mongoid::Document # Fields - field :_id, :type => String + field :_id, :type => String, :overwrite => true field :country, :type => String field :region, :type => String field :city, :type => String diff --git a/src/api-umbrella/web-app/app/models/log_result/base.rb b/src/api-umbrella/web-app/app/models/log_result/base.rb index 46f4df95f..f7c02716e 100644 --- a/src/api-umbrella/web-app/app/models/log_result/base.rb +++ b/src/api-umbrella/web-app/app/models/log_result/base.rb @@ -65,7 +65,7 @@ def map_breadcrumbs @map_breadcrumbs = [ { :region => "world", :name => "World" }, - { :name => Country[country].name }, + { :name => ISO3166::Country.new(country).name }, ] when /^(US)-([A-Z]{2})$/ country = Regexp.last_match[1] @@ -73,8 +73,8 @@ def map_breadcrumbs @map_breadcrumbs = [ { :region => "world", :name => "World" }, - { :region => country, :name => Country[country].name }, - { :name => Country[country].states[state]["name"] }, + { :region => country, :name => ISO3166::Country.new(country).name }, + { :name => ISO3166::Country.new(country).states[state]["name"] }, ] end end diff --git a/src/api-umbrella/web-app/app/models/log_result/elastic_search.rb b/src/api-umbrella/web-app/app/models/log_result/elastic_search.rb index ae9958dd4..d7c39e1fa 100644 --- a/src/api-umbrella/web-app/app/models/log_result/elastic_search.rb +++ b/src/api-umbrella/web-app/app/models/log_result/elastic_search.rb @@ -1,7 +1,7 @@ class LogResult::ElasticSearch < LogResult::Base def bulk_each scroll_id = raw_result["_scroll_id"] - while(scroll = @search.client.scroll(:scroll_id => scroll_id, :scroll => "10m")) # rubocop:disable Lint/LiteralInCondition + while(scroll = @search.client.scroll(:scroll_id => scroll_id, :scroll => "10m")) # rubocop:disable Lint/AssignmentInCondition scroll_id = scroll["_scroll_id"] hits = scroll["hits"]["hits"] diff --git a/src/api-umbrella/web-app/app/models/log_search/base.rb b/src/api-umbrella/web-app/app/models/log_search/base.rb index 85b0a5aef..54b03322b 100644 --- a/src/api-umbrella/web-app/app/models/log_search/base.rb +++ b/src/api-umbrella/web-app/app/models/log_search/base.rb @@ -7,7 +7,7 @@ class LogSearch::Base "request_ip_country", "request_ip_region", "request_ip_city", - ] + ].freeze def self.policy_class # Set the Pundit policy class to be the same for all LogSearch::Base child diff --git a/src/api-umbrella/web-app/app/models/log_search/elastic_search.rb b/src/api-umbrella/web-app/app/models/log_search/elastic_search.rb index ee5895d86..35a96ee6f 100644 --- a/src/api-umbrella/web-app/app/models/log_search/elastic_search.rb +++ b/src/api-umbrella/web-app/app/models/log_search/elastic_search.rb @@ -9,7 +9,7 @@ def initialize(options = {}) @client = ::Elasticsearch::Client.new({ :hosts => ApiUmbrellaConfig[:elasticsearch][:hosts], - :logger => Rails.logger + :logger => Rails.logger, }) @query = { @@ -58,7 +58,7 @@ def result def permission_scope!(scopes) filter = { :bool => { - :should => [] + :should => [], }, } @@ -77,7 +77,7 @@ def search!(query_string) if(query_string.present?) @query[:query][:filtered][:query] = { :query_string => { - :query => query_string + :query => query_string, }, } end @@ -188,7 +188,7 @@ def parse_query_builder(query) condition = if(query["condition"] == "OR") then :should else :must end query_filter = { :bool => { - condition => filters + condition => filters, }, } end @@ -273,7 +273,7 @@ def aggregate_by_drilldown!(prefix, size = 0) end def aggregate_by_drilldown_over_time!(prefix) - @query[:query][:filtered][:filter][:bool][:must] << { + @query[:query][:filtered][:filter][:bool][:must] << { :prefix => { :request_hierarchy => prefix, }, diff --git a/src/api-umbrella/web-app/app/models/log_search/sql.rb b/src/api-umbrella/web-app/app/models/log_search/sql.rb index a24186f52..519801b7b 100644 --- a/src/api-umbrella/web-app/app/models/log_search/sql.rb +++ b/src/api-umbrella/web-app/app/models/log_search/sql.rb @@ -3,7 +3,7 @@ class LogSearch::Sql < LogSearch::Base NOT_NULL_FIELDS = [ "request_ip", - ] + ].freeze LEGACY_FIELDS = { "backend_response_time" => "timer_backend_response", @@ -16,11 +16,11 @@ class LogSearch::Sql < LogSearch::Base "request_path" => "request_url_path", "request_scheme" => "request_url_scheme", "response_time" => "timer_response", - } + }.freeze FIELD_TYPES = { "response_status" => :int, - } + }.freeze def initialize(options = {}) super diff --git a/src/api-umbrella/web-app/app/models/website_backend.rb b/src/api-umbrella/web-app/app/models/website_backend.rb index a3f90530c..aa0600885 100644 --- a/src/api-umbrella/web-app/app/models/website_backend.rb +++ b/src/api-umbrella/web-app/app/models/website_backend.rb @@ -8,7 +8,7 @@ class WebsiteBackend include Mongoid::Delorean::Trackable # Fields - field :_id, :type => String, :default => lambda { UUIDTools::UUID.random_create.to_s } + field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :frontend_host, :type => String field :backend_protocol, :type => String field :server_host, :type => String diff --git a/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb index b68713f29..278494657 100644 --- a/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb +++ b/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb @@ -1,67 +1,52 @@ -
        -
        -

        Admin Login

        -
        -
        +
        +

        Admin Login

        -<% if(!resource_class.omniauth_providers.include?(:developer) && Admin.count == 0) %> -
        - No admins currently exist.
        - Initial admin accounts may be defined in /etc/api-umbrella/api-umbrella.yml (see the web.admin.initial_superusers section) -
        -<% end %> + <% if(!resource_class.omniauth_providers.include?(:developer) && Admin.count == 0) %> +
        + No admins currently exist.
        + Initial admin accounts may be defined in /etc/api-umbrella/api-umbrella.yml (see the web.admin.initial_superusers section) +
        + <% end %> -
        - <%- resource_class.omniauth_providers.each do |provider| %> - <% if(provider == :persona) %> -
        - -
        -
        - -
        -
        -
        - <% else %> - <% strategy = Devise.omniauth_configs[provider].strategy %> - <% if(strategy.respond_to?(:client_secret) && strategy.client_secret.blank?) %> -
        - <%= t(provider, :scope => [:omniauth_providers]) %> authentication provider not yet configured.
        - Access tokens must be defined in /etc/api-umbrella/api-umbrella.yml (see the web.admin.auth_strategies section) -
        -
        -
        - -
        -
        - <% else %> -
        -
        - <%= link_to "Login with #{t(provider, :scope => [:omniauth_providers])}", omniauth_authorize_path(resource_name, provider), :class => "btn btn-primary btn-block" %> -
        -
        - <% end %> - <% end %> - <% end -%> -
        +
        + <%- resource_class.omniauth_providers.each do |provider| %> + + <% end -%> +
        +
        diff --git a/src/api-umbrella/web-app/app/views/layouts/admin.html.erb b/src/api-umbrella/web-app/app/views/layouts/admin.html.erb deleted file mode 100644 index 757cc830b..000000000 --- a/src/api-umbrella/web-app/app/views/layouts/admin.html.erb +++ /dev/null @@ -1,98 +0,0 @@ - - - - - API Umbrella Admin - <%= stylesheet_link_tag "admin" %> - <%= javascript_tag do %> - webAdminAjaxApiKey = <%= web_admin_ajax_api_user.api_key.to_json.html_safe %>; - currentAdmin = <%= current_admin.attributes.slice("username", "superuser").to_json.html_safe %>; - enableBetaAnalytics = <%= (ApiUmbrellaConfig[:analytics][:adapter] == "kylin" || (ApiUmbrellaConfig[:analytics][:outputs] && ApiUmbrellaConfig[:analytics][:outputs].include?("kylin"))).to_json.html_safe %>; - <% end %> - - <%= javascript_include_tag "admin/locales/#{I18n.locale}" %> - <%= javascript_include_tag "admin" %> - <%= javascript_include_tag "//www.google.com/jsapi" %> - <%= javascript_tag do %> - google.load("visualization", "1", {packages:["corechart", "geochart"]}); - <% end %> - <%= csrf_meta_tag %> - <% if(Rails.env.test?) %> - <%= stylesheet_link_tag "admin_test" %> - <%= javascript_include_tag "admin_test" %> - <% end %> - - - - -
        - <% flash.each do |flash_type, message| %> -
        - <%= message %> -
        - <% end %> - - <%= yield %> -
        - - - - - diff --git a/src/api-umbrella/web-app/app/views/layouts/application.html.erb b/src/api-umbrella/web-app/app/views/layouts/application.html.erb index 80818e440..6ec64bc94 100644 --- a/src/api-umbrella/web-app/app/views/layouts/application.html.erb +++ b/src/api-umbrella/web-app/app/views/layouts/application.html.erb @@ -3,16 +3,16 @@ API Umbrella - <%= stylesheet_link_tag "application" %> - <%= javascript_include_tag "application" %> - <%= csrf_meta_tag %> + + <%= stylesheet_link_tag "admin/login" %> + <%= csrf_meta_tags %> -
        -
        +
        +
        <% flash.each do |flash_type, message| %> -
        - <%= message %> +
        + <%= message.html_safe %>
        <% end %> diff --git a/src/api-umbrella/web-app/bin/bundle b/src/api-umbrella/web-app/bin/bundle new file mode 100755 index 000000000..66e9889e8 --- /dev/null +++ b/src/api-umbrella/web-app/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/src/api-umbrella/web-app/script/delayed_job b/src/api-umbrella/web-app/bin/delayed_job similarity index 100% rename from src/api-umbrella/web-app/script/delayed_job rename to src/api-umbrella/web-app/bin/delayed_job diff --git a/src/api-umbrella/web-app/bin/rails b/src/api-umbrella/web-app/bin/rails new file mode 100755 index 000000000..0138d79b7 --- /dev/null +++ b/src/api-umbrella/web-app/bin/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +APP_PATH = File.expand_path('../../config/application', __FILE__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/src/api-umbrella/web-app/bin/rake b/src/api-umbrella/web-app/bin/rake new file mode 100755 index 000000000..d87d5f578 --- /dev/null +++ b/src/api-umbrella/web-app/bin/rake @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/src/api-umbrella/web-app/bin/setup b/src/api-umbrella/web-app/bin/setup new file mode 100755 index 000000000..acdb2c138 --- /dev/null +++ b/src/api-umbrella/web-app/bin/setup @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +Dir.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file: + + puts "== Installing dependencies ==" + system "gem install bundler --conservative" + system "bundle check || bundle install" + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # system "cp config/database.yml.sample config/database.yml" + # end + + puts "\n== Preparing database ==" + system "bin/rake db:setup" + + puts "\n== Removing old logs and tempfiles ==" + system "rm -f log/*" + system "rm -rf tmp/cache" + + puts "\n== Restarting application server ==" + system "touch tmp/restart.txt" +end diff --git a/src/api-umbrella/web-app/bin/spring b/src/api-umbrella/web-app/bin/spring new file mode 100755 index 000000000..7fe232c3a --- /dev/null +++ b/src/api-umbrella/web-app/bin/spring @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) + Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } + gem 'spring', match[1] + require 'spring/binstub' + end +end diff --git a/src/api-umbrella/web-app/config.ru b/src/api-umbrella/web-app/config.ru index 4bc289300..bd83b2541 100644 --- a/src/api-umbrella/web-app/config.ru +++ b/src/api-umbrella/web-app/config.ru @@ -1,4 +1,4 @@ # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) -run ApiUmbrella::Application +require ::File.expand_path('../config/environment', __FILE__) +run Rails.application diff --git a/src/api-umbrella/web-app/config/application.rb b/src/api-umbrella/web-app/config/application.rb index 8745bd3f2..22cf8ac24 100644 --- a/src/api-umbrella/web-app/config/application.rb +++ b/src/api-umbrella/web-app/config/application.rb @@ -1,25 +1,25 @@ require File.expand_path('../boot', __FILE__) -# Pick the frameworks you want: -# require "active_record/railtie" require "action_controller/railtie" +require "action_view/railtie" require "action_mailer/railtie" -require "active_resource/railtie" +require "active_job/railtie" require "sprockets/railtie" -# require "rails/test_unit/railtie" -if defined?(Bundler) - # If you precompile assets before deploying to production, use this line - Bundler.require(*Rails.groups(:assets => %w(development test))) - # If you want your assets lazily compiled in production, use this line - # Bundler.require(:default, :assets, Rails.env) -end +# Log to stdout when running as a server process (like all the other perpd +# processes API Umbrella manages). +require "rails_stdout_logging" if(ENV["RAILS_LOG_TO_STDOUT"].present?) + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) module ApiUmbrella class Application < Rails::Application - config.before_configuration do - require "symbolize_helper" + config.autoload_paths += ["#{config.root}/lib"] + config.eager_load_paths += ["#{config.root}/lib"] + config.before_configuration do config_files = [] config_file = ENV["API_UMBRELLA_RUNTIME_CONFIG"] if(config_file.present?) @@ -58,39 +58,10 @@ class Application < Rails::Application # Load the YAML config in. config = {} config_files.each do |file| - data = SymbolizeHelper.symbolize_recursive(YAML.load_file(file)) + data = YAML.load_file(file).deep_symbolize_keys config.deep_merge!(data) end - if(Rails.env == "test") - # When running as part of the integration test suite, where we run all - # the API Umbrella processes separately, ensure we connect to those - # ports. - if(ENV["INTEGRATION_TEST_SUITE"]) - config[:mongodb][:url] = "mongodb://127.0.0.1:13001/api_umbrella_test" - - # Don't override the Elasticsearch v2 connection tests. - if(config[:elasticsearch][:hosts] != ["http://127.0.0.1:9200"]) - config[:elasticsearch][:hosts] = ["http://127.0.0.1:13002"] - end - - # If not running as part of the integration test suite, then we assume - # a developer is just running the rails tests a standalone command. In - # that case, we'll connect to the default API Umbrella ports for - # databases that we assume are running in the development environment. - # The only difference is MongoDB, where we want to make sure we connect - # to a separate test database so tests don't interfere with - # development. - elsif(!ENV["FULL_STACK_TEST"]) - config[:mongodb][:url] = "mongodb://127.0.0.1:14001/api_umbrella_test" - - # Don't override the Elasticsearch v2 connection tests. - if(config[:elasticsearch][:hosts] != ["http://127.0.0.1:9200"]) - config[:elasticsearch][:hosts] = ["http://127.0.0.1:14002"] - end - end - end - # Set the default host used for web application links (for mailers, # contact URLs, etc). # @@ -116,20 +87,32 @@ class Application < Rails::Application require "js_locale_helper" end + # Instead of loading from a mongoid.yml file, load the Mongoid config in + # code, where it's easier to merge settings from our API Umbrella + # configuration. + initializer "mongoid-config", :after => "mongoid.load-config" do + config = { + :clients => { + :default => { + :uri => ApiUmbrellaConfig[:mongodb][:url], + :options => { + :read => { + :mode => ApiUmbrellaConfig[:mongodb][:read_preference].to_s.underscore.to_sym, + }, + }, + }, + }, + } + + Mongoid::Clients.disconnect + Mongoid::Clients.clear + Mongoid.load_configuration(config) + end + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %W(#{config.root}/app/workers) - - # Only load the plugins named here, in the order given (default is alphabetical). - # :all can be used as a placeholder for all plugins not explicitly named. - # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - - # Activate observers that should always be running. - # config.active_record.observers = :cacher, :garbage_collector, :forum_observer - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' @@ -137,98 +120,29 @@ class Application < Rails::Application # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de - config.i18n.enforce_available_locales = true - - # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = "utf-8" - - # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] - - # Enable escaping HTML in JSON. - config.active_support.escape_html_entities_in_json = true - - # Use SQL instead of Active Record's schema dumper when creating the database. - # This is necessary if your schema can't be completely dumped by the schema dumper, - # like if you have constraints or database-specific column types - # config.active_record.schema_format = :sql - - # Enforce whitelist mode for mass assignment. - # This will create an empty whitelist of attributes available for mass-assignment for all models - # in your app. As such, your models will need to explicitly whitelist or blacklist accessible - # parameters by using an attr_accessible or attr_protected declaration. - # config.active_record.whitelist_attributes = true if(ENV["RAILS_TMP_PATH"].present?) - paths["tmp"] = ENV["RAILS_TMP_PATH"] - tmp_assets_cache_path = File.join(ENV["RAILS_TMP_PATH"], "cache/assets") - FileUtils.mkdir_p(tmp_assets_cache_path) - config.assets.cache_store = [:file_store, tmp_assets_cache_path] - config.sass.cache_location = File.join(ENV["RAILS_TMP_PATH"], "cache/sass") + config.paths["tmp"] = ENV["RAILS_TMP_PATH"] + config.assets.configure do |env| + env.cache = Sprockets::Cache::FileStore.new( + File.join(ENV["RAILS_TMP_PATH"], "cache/assets"), + config.assets.cache_limit, + env.logger, + ) + end end if(ENV["RAILS_PUBLIC_PATH"].present?) - paths["public"] = ENV["RAILS_PUBLIC_PATH"] - end - - # Enable the asset pipeline - config.assets.enabled = true - - # Version of your assets, change this if you want to expire all your assets - config.assets.version = '1.0' - - # Include the config/locales directory on the asset path so that the locale - # YAML files can be setup as "depend_on" dependencies inside assets (for - # use with JsLocaleHelper). - config.assets.paths += %W(#{config.root}/config/locales) - - # Choose the compressors to use - config.assets.js_compressor = :uglifier - - # Rely on Sass's built-in compressor for CSS minifying. - # config.assets.css_compressor = :yui - - # Move default assets directory so this project can co-exist with the - # static-site projectt that delivers most of the web content. - config.assets.prefix = "/web-assets" - - # Reset the default precompile list list to exclude our vendored submodule - # stuff. This should go away in Rails 4, where vendor/assets is - # automatically excluded. - # Based on the original here: - # https://github.com/rails/rails/blob/v3.2.17/railties/lib/rails/application/configuration.rb#L48-L49 - config.assets.precompile = [ - proc do |path| - !File.extname(path).in?(['.js', '.css']) && path !~ /^vendor/ - end, - /(?:\/|\\|\A)application\.(css|js)$/, - ] - - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - config.assets.precompile += %w( - admin.css - admin.js - admin/stats.js - ie_lt_9.js - ) - - # Detect and precompile all the locale assets. - Dir.glob("#{config.root}/app/assets/javascripts/admin/locales/*.js.erb").each do |path| - config.assets.precompile << path.gsub(%r{^.*/app/assets/javascripts/}, "").gsub(/\.erb$/, "") - end - - # Setup ember and handlebars, but only when it's being required (so - # development and test, but only production when running assets:precompile - # with RAILS_GROUPS=assets). - if(config.respond_to?(:ember)) - config.ember.variant = :development - config.handlebars.templates_root = ["admin/templates", "templates"] + config.paths["public"] = ENV["RAILS_PUBLIC_PATH"] end # Use a mongo-based cache store (this ensures the cache can be shared # amongst multiple servers). config.cache_store = :mongoid_store + # Use delayed job for background jobs. + config.active_job.queue_adapter = :delayed_job + config.action_mailer.raise_delivery_errors = true config.action_mailer.default_url_options = { :host => ApiUmbrellaConfig[:web][:default_host], @@ -237,5 +151,7 @@ class Application < Rails::Application if(ApiUmbrellaConfig[:web] && ApiUmbrellaConfig[:web][:mailer] && ApiUmbrellaConfig[:web][:mailer][:smtp_settings]) config.action_mailer.smtp_settings = ApiUmbrellaConfig[:web][:mailer][:smtp_settings] end + + config.middleware.use HttpAcceptLanguage::Middleware end end diff --git a/src/api-umbrella/web-app/config/boot.rb b/src/api-umbrella/web-app/config/boot.rb index f2830ae31..6b750f00b 100644 --- a/src/api-umbrella/web-app/config/boot.rb +++ b/src/api-umbrella/web-app/config/boot.rb @@ -1,6 +1,3 @@ -require 'rubygems' - -# Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/src/api-umbrella/web-app/config/compass.rb b/src/api-umbrella/web-app/config/compass.rb deleted file mode 100644 index 73a4a070b..000000000 --- a/src/api-umbrella/web-app/config/compass.rb +++ /dev/null @@ -1,4 +0,0 @@ -project_type = :rails - -# Separate the generated sprites from the normal image assets. -generated_images_dir = "app/assets/generated_sprites" diff --git a/src/api-umbrella/web-app/config/environment.rb b/src/api-umbrella/web-app/config/environment.rb index c3cddccd3..ee8d90dc6 100644 --- a/src/api-umbrella/web-app/config/environment.rb +++ b/src/api-umbrella/web-app/config/environment.rb @@ -1,5 +1,5 @@ -# Load the rails application +# Load the Rails application. require File.expand_path('../application', __FILE__) -# Initialize the rails application -ApiUmbrella::Application.initialize! +# Initialize the Rails application. +Rails.application.initialize! diff --git a/src/api-umbrella/web-app/config/environments/development.rb b/src/api-umbrella/web-app/config/environments/development.rb index ced7cd8c6..0feba2ef1 100644 --- a/src/api-umbrella/web-app/config/environments/development.rb +++ b/src/api-umbrella/web-app/config/environments/development.rb @@ -1,29 +1,38 @@ -ApiUmbrella::Application.configure do - # Settings specified here will take precedence over those in config/application.rb +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false - # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true + # Do not eager load code on boot. + config.eager_load = false - # Show full error reports + # Show full error reports and disable caching. config.consider_all_requests_local = true + config.action_controller.perform_caching = false - # Enable caching, so we can properly develop accounting for the cache. - config.action_controller.perform_caching = true + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false - # Print deprecation notices to the Rails logger + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log - # Only use best-standards-support built into browsers - config.action_dispatch.best_standards_support = :builtin + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true - # Do not compress assets - config.assets.compress = false + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. + config.assets.digest = true - # Expands the lines which load the assets - config.assets.debug = true + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true end diff --git a/src/api-umbrella/web-app/config/environments/omnibus.rb b/src/api-umbrella/web-app/config/environments/omnibus.rb deleted file mode 100644 index 9cb39a003..000000000 --- a/src/api-umbrella/web-app/config/environments/omnibus.rb +++ /dev/null @@ -1,64 +0,0 @@ -ApiUmbrella::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - # Code is not reloaded between requests - config.cache_classes = true - - # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_assets = false - - # Compress JavaScripts and CSS - config.assets.compress = true - - # Don't fallback to assets pipeline if a precompiled asset is missed - config.assets.compile = false - - # Generate digests for assets URLs - config.assets.digest = true - - # Defaults to nil and saved in location specified by config.assets.prefix - # config.assets.manifest = YOUR_PATH - - # Specifies the header that your server uses for sending files - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # See everything in the log (default is :info) - # config.log_level = :debug - - # Prepend all log lines with the following tags - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production - # config.cache_store = :mem_cache_store - - # Enable serving of images, stylesheets, and JavaScripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" - - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile += %w( search.js ) - - # Disable delivery errors, bad email addresses will be ignored - # config.action_mailer.raise_delivery_errors = false - - # Enable threaded mode - config.threadsafe! - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true - - # Send deprecation notices to registered listeners - config.active_support.deprecation = :notify - -end diff --git a/src/api-umbrella/web-app/config/environments/production.rb b/src/api-umbrella/web-app/config/environments/production.rb index 9cb39a003..d97d7dffe 100644 --- a/src/api-umbrella/web-app/config/environments/production.rb +++ b/src/api-umbrella/web-app/config/environments/production.rb @@ -1,64 +1,76 @@ -ApiUmbrella::Application.configure do - # Settings specified here will take precedence over those in config/application.rb +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. - # Code is not reloaded between requests + # Code is not reloaded between requests. config.cache_classes = true - # Full error reports are disabled and caching is turned on + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_assets = false + # Enable Rack::Cache to put a simple HTTP cache in front of your application + # Add `rack-cache` to your Gemfile before enabling this. + # For large-scale production use, consider using a caching reverse proxy like + # NGINX, varnish or squid. + # config.action_dispatch.rack_cache = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? - # Compress JavaScripts and CSS - config.assets.compress = true + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass - # Don't fallback to assets pipeline if a precompiled asset is missed + # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Generate digests for assets URLs + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. config.assets.digest = true - # Defaults to nil and saved in location specified by config.assets.prefix - # config.assets.manifest = YOUR_PATH + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - # Specifies the header that your server uses for sending files - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true - # See everything in the log (default is :info) - # config.log_level = :debug + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :info - # Prepend all log lines with the following tags + # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] - # Use a different logger for distributed setups + # Use a different logger for distributed setups. # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - # Use a different cache store in production + # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile += %w( search.js ) - - # Disable delivery errors, bad email addresses will be ignored + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false - # Enable threaded mode - config.threadsafe! - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) + # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true - # Send deprecation notices to registered listeners + # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new end diff --git a/src/api-umbrella/web-app/config/environments/staging.rb b/src/api-umbrella/web-app/config/environments/staging.rb deleted file mode 100644 index 9cb39a003..000000000 --- a/src/api-umbrella/web-app/config/environments/staging.rb +++ /dev/null @@ -1,64 +0,0 @@ -ApiUmbrella::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - # Code is not reloaded between requests - config.cache_classes = true - - # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_assets = false - - # Compress JavaScripts and CSS - config.assets.compress = true - - # Don't fallback to assets pipeline if a precompiled asset is missed - config.assets.compile = false - - # Generate digests for assets URLs - config.assets.digest = true - - # Defaults to nil and saved in location specified by config.assets.prefix - # config.assets.manifest = YOUR_PATH - - # Specifies the header that your server uses for sending files - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # See everything in the log (default is :info) - # config.log_level = :debug - - # Prepend all log lines with the following tags - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production - # config.cache_store = :mem_cache_store - - # Enable serving of images, stylesheets, and JavaScripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" - - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile += %w( search.js ) - - # Disable delivery errors, bad email addresses will be ignored - # config.action_mailer.raise_delivery_errors = false - - # Enable threaded mode - config.threadsafe! - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true - - # Send deprecation notices to registered listeners - config.active_support.deprecation = :notify - -end diff --git a/src/api-umbrella/web-app/config/environments/test.rb b/src/api-umbrella/web-app/config/environments/test.rb index 0fb430c52..51d091fa1 100644 --- a/src/api-umbrella/web-app/config/environments/test.rb +++ b/src/api-umbrella/web-app/config/environments/test.rb @@ -1,62 +1,42 @@ -ApiUmbrella::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true - - # Configure static asset server for tests with Cache-Control for performance - config.serve_static_assets = true - config.static_cache_control = "public, max-age=3600" - - # Log error messages when you accidentally call methods on nil - config.whiny_nils = true - - # Show full error reports and disable caching - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Raise exceptions instead of rendering exception templates - config.action_dispatch.show_exceptions = true - - # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - config.action_mailer.delivery_method = :test +# Since we're performing all our tests as full-stack integration tests, use the +# production settings as the defaults, so the tests are what we'll see in +# production. +require_relative "./production.rb" + +Rails.application.configure do + config.log_level = :debug + + # Deliver real e-mail if running integration tests with local MailHog as our + # test SMTP server. + if(!config.action_mailer.smtp_settings || config.action_mailer.smtp_settings[:address] != "127.0.0.1" || config.action_mailer.smtp_settings[:port] != 13102) + config.action_mailer.delivery_method = :test + end - # Print deprecation notices to the stderr + # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr +end - if(ENV["FULL_STACK_TEST"].to_s == "true") - config.consider_all_requests_local = false - elsif(ENV["PRECOMPILE_TEST_ASSETS"].to_s != "false") - # Use precompiled assets in test mode so we can properly catch errors - # triggered by not having assets in the precompile list. - config.assets.compile = false - config.assets.digest = true - config.assets.prefix = "/test-assets" - - # Precompile additional assets for the test environment. - config.assets.precompile += %w( - admin_test.css - admin_test.js - ) +if(Rails.env.test?) + # For the test environment setup a middleware that looks for the + # "test_delay_server_responses" cookie on requests, and if it's set, sleeps + # for that amount of time before returning responses. + # + # This can be used for some Capybara integration tests that otherwise might + # happen too quickly (for example, checking that a loading spinner pops up + # while making an ajax request). + class TestDelayServerResponses + def initialize(app) + @app = app + end - config.before_initialize do |app| - # Run the asset precompile phase prior to running tests so that we can test - # with precompiled assets (so we're more properly testing for any potential - # missing precompiled assets). However, note that we have to call this - # here, instead of in something like an rspec before suite so that the - # precompile happens early enough so that the current environment can load - # the precompiled digests. - unless ENV["PRECOMPILE_TEST_ASSETS"] - system("RAILS_ENV=test PRECOMPILE_TEST_ASSETS=true bundle exec rake assets:precompile") + def call(env) + request = ActionDispatch::Request.new(env) + if(request.cookies["test_delay_server_responses"].present?) + sleep(request.cookies["test_delay_server_responses"].to_f) end + + @app.call(env) end end + Rails.application.config.middleware.use(TestDelayServerResponses) end diff --git a/src/api-umbrella/web-app/config/initializers/assets.rb b/src/api-umbrella/web-app/config/initializers/assets.rb new file mode 100644 index 000000000..afdc1c5ad --- /dev/null +++ b/src/api-umbrella/web-app/config/initializers/assets.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +Rails.application.config.assets.precompile += [ + "admin/login.css", + "admin/server_side_loader.js", +] + +# Move default assets directory so this project can co-exist with the +# static-site projectt that delivers most of the web content. +Rails.application.config.assets.prefix = "/web-assets" + +# Generate non-cached busted versions of assets that the admin-ui app needs to +# link directly to (since it has no knowledge of the cache-busted URLs). We +# just need to make sure these assets aren't allowed to be cached by the +# browser. +# +# This is used for sharing i18n data between the Rails and Ember app. While not +# the most optimized solution, it should be fine for sharing this bit of data. +NonStupidDigestAssets.whitelist += [ + "admin/server_side_loader.js", +] diff --git a/src/api-umbrella/web-app/config/initializers/backtrace_silencers.rb b/src/api-umbrella/web-app/config/initializers/backtrace_silencers.rb index 56ddc8da8..59385cdf3 100644 --- a/src/api-umbrella/web-app/config/initializers/backtrace_silencers.rb +++ b/src/api-umbrella/web-app/config/initializers/backtrace_silencers.rb @@ -4,4 +4,4 @@ # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -Rails.backtrace_cleaner.remove_silencers! +# Rails.backtrace_cleaner.remove_silencers! diff --git a/src/api-umbrella/web-app/config/initializers/bootstrap_navbar_tab_builder.rb b/src/api-umbrella/web-app/config/initializers/bootstrap_navbar_tab_builder.rb deleted file mode 100644 index ceddad299..000000000 --- a/src/api-umbrella/web-app/config/initializers/bootstrap_navbar_tab_builder.rb +++ /dev/null @@ -1,13 +0,0 @@ -class BootstrapNavbarTabBuilder < TabsOnRails::Tabs::TabsBuilder - def tab_for(tab, name, url_options, item_options = {}, &block) - item_options[:class] = item_options[:class].to_s.split(" ").push("active").join(" ") if current_tab?(tab) - - link_html_options = item_options.delete(:link_html) - content = @context.link_to(name, url_options, link_html_options) - if block - content << @context.capture(&block) - end - - @context.content_tag(:li, content, item_options) - end -end diff --git a/src/api-umbrella/web-app/config/initializers/cookies_serializer.rb b/src/api-umbrella/web-app/config/initializers/cookies_serializer.rb new file mode 100644 index 000000000..7f70458de --- /dev/null +++ b/src/api-umbrella/web-app/config/initializers/cookies_serializer.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/src/api-umbrella/web-app/config/initializers/delayed_job.rb b/src/api-umbrella/web-app/config/initializers/delayed_job.rb index ddf892ce8..cae8a5f3f 100644 --- a/src/api-umbrella/web-app/config/initializers/delayed_job.rb +++ b/src/api-umbrella/web-app/config/initializers/delayed_job.rb @@ -1 +1,7 @@ Delayed::Worker.destroy_failed_jobs = false + +if(Rails.env.test?) + # Check for jobs more frequently in the test environment to cut down on wait + # times for integration tests. + Delayed::Worker.sleep_delay = 0.2 +end diff --git a/src/api-umbrella/web-app/config/initializers/devise.rb b/src/api-umbrella/web-app/config/initializers/devise.rb index 2ac68bcaf..5103cdae3 100644 --- a/src/api-umbrella/web-app/config/initializers/devise.rb +++ b/src/api-umbrella/web-app/config/initializers/devise.rb @@ -4,13 +4,9 @@ # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. - development_secret_key = "fbafe6a3663ac3f7b43000ff02c4cf0f64019007113ffa88606e1a41c2ff458b322384d79c362bf116eb777d2db3fab8848fd9020f9e8bdb211677ed91f14b27" - config.secret_key = ENV["DEVISE_SECRET_KEY"] || ApiUmbrellaConfig[:web][:devise_secret_key] || development_secret_key - if(!%w(development test).include?(Rails.env)) - if(config.secret_key == development_secret_key) - raise "An insecure secret token is being used. Please set the DEVISE_SECRET_KEY environment variable with your own private key. Run 'rake secret_keys:generate' for more details." - end - end + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '45636489568f91c247a264a864b8a97303dac845baaa6bf91fc264126ddd095f572e751e94f665ea5a038da91531d8084ea2fa17370751912b5749dbd3a0d32d' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, @@ -21,6 +17,9 @@ # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be @@ -35,7 +34,7 @@ # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [ :email ] + # config.authentication_keys = [:email] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the @@ -67,7 +66,7 @@ # :database = Support basic authentication with authentication key + password # config.http_authenticatable = false - # If http headers should be returned for AJAX requests. True by default. + # If 401 status code should be returned for AJAX requests. True by default. # config.http_authenticatable_on_xhr = true # The realm used in Http Basic Authentication. 'Application' by default. @@ -82,7 +81,7 @@ # particular strategies by setting this option. # Notice that if you are skipping storage for all authentication paths, you # may want to disable generating routes to Devise's sessions controller by - # passing :skip => :sessions to `devise_for` in your config/routes.rb + # passing skip: :sessions to `devise_for` in your config/routes.rb config.skip_session_storage = [:http_auth] # By default, Devise cleans up the CSRF token on authentication to @@ -91,24 +90,35 @@ # from the server. You can disable this option at your own risk. # config.clean_up_csrf_token_on_authentication = true + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + # ==> Configuration for :database_authenticatable - # For bcrypt, this is the cost for hashing the password and defaults to 10. If - # using other encryptors, it sets how many times you want the password re-encrypted. + # For bcrypt, this is the cost for hashing the password and defaults to 11. If + # using other algorithms, it sets how many times you want the password to be hashed. # # Limiting the stretches to just one in testing will increase the performance of # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use - # a value less than 10 in other environments. - config.stretches = Rails.env.test? ? 1 : 10 + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 11 - # Setup a pepper to generate the encrypted password. - # config.pepper = '066be53fcbb194155b66145c093269b3ca1a484ac08b520709d26061a0d9abde11d8f572f1b756e9903ac44bbb4af46855d1db880d72d6b8ec8d33d2470b16af' + # Set up a pepper to generate the hashed password. + # config.pepper = '4cec288f0a01acf8450902e2fa70c6bec7ae2e6053bbf54c502b70a700020863b46e32aceab69d52df637db8473e55a85c3aca79b6ecc42f8f26091e6024fd92' + + # Send a notification email when the user's password is changed + # config.send_password_change_notification = false # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without - # confirming his account. For instance, if set to 2.days, the user will be - # able to access the website for two days without confirming his account, + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, # access will be blocked just in the third day. Default is 0.days, meaning - # the user cannot access the website without confirming his account. + # the user cannot access the website without confirming their account. # config.allow_unconfirmed_access_for = 2.days # A period that the user is allowed to confirm their account before their @@ -121,41 +131,41 @@ # If true, requires any email changes to be confirmed (exactly the same way as # initial account confirmation) to be applied. Requires additional unconfirmed_email - # db field (see migrations). Until confirmed new email is stored in - # unconfirmed email column, and copied to email column on successful confirmation. + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. config.reconfirmable = true # Defines which key will be used when confirming an account - # config.confirmation_keys = [ :email ] + # config.confirmation_keys = [:email] # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false # Options to be passed to the created cookie. For instance, you can set - # :secure => true in order to force SSL only cookies. + # secure: true in order to force SSL only cookies. # config.rememberable_options = {} # ==> Configuration for :validatable - # Range for password length. Default is 8..128. - config.password_length = 8..128 + # Range for password length. + config.password_length = 6..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly # to give user feedback and not to assert the e-mail validity. - # config.email_regexp = /\A[^@]+@[^@]+\z/ + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. # config.timeout_in = 30.minutes - # If true, expires auth token on session timeout. - # config.expire_auth_token_on_timeout = false - # ==> Configuration for :lockable # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. @@ -163,7 +173,7 @@ # config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [ :email ] + # config.unlock_keys = [:email] # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email @@ -180,24 +190,28 @@ # config.unlock_in = 1.hour # Warn on the last attempt before the account is locked. - # config.last_attempt_warning = false + # config.last_attempt_warning = true # ==> Configuration for :recoverable # # Defines which key will be used when recovering the password for an account - # config.reset_password_keys = [ :email ] + # config.reset_password_keys = [:email] # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. config.reset_password_within = 6.hours + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + # ==> Configuration for :encryptable - # Allow you to use another encryption algorithm besides bcrypt (default). You can use - # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, - # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) - # and :restful_authentication_sha1 (then you should set stretches to 10, and copy - # REST_AUTH_SITE_KEY to pepper). + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). # # Require the `devise-encryptable` gem when using anything other than bcrypt # config.encryptor = :sha512 @@ -233,8 +247,7 @@ # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' - + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' if(%w(development test).include?(Rails.env)) config.omniauth :developer, :fields => [:email] @@ -243,39 +256,41 @@ ApiUmbrellaConfig[:web][:admin][:auth_strategies][:enabled].each do |strategy| case(strategy) when "facebook" + require "omniauth-facebook" config.omniauth :facebook, ApiUmbrellaConfig[:web][:admin][:auth_strategies][:facebook][:client_id], ApiUmbrellaConfig[:web][:admin][:auth_strategies][:facebook][:client_secret], :scope => "email" when "cas" + require "omniauth-cas" config.omniauth :cas, ApiUmbrellaConfig[:web][:admin][:auth_strategies][:cas][:options] when "github" + require "omniauth-github" config.omniauth :github, ApiUmbrellaConfig[:web][:admin][:auth_strategies][:github][:client_id], ApiUmbrellaConfig[:web][:admin][:auth_strategies][:github][:client_secret], :scope => "user:email" when "google" + require "omniauth-google-oauth2" config.omniauth :google_oauth2, ApiUmbrellaConfig[:web][:admin][:auth_strategies][:google][:client_id], ApiUmbrellaConfig[:web][:admin][:auth_strategies][:google][:client_secret], :scope => "userinfo.email" when "ldap" + require "omniauth-ldap" config.omniauth :ldap, ApiUmbrellaConfig[:web][:admin][:auth_strategies][:ldap][:options] when "max.gov" + require "omniauth-cas" config.omniauth :cas, :host => "login.max.gov", :login_url => "/cas/login", :service_validate_url => "/cas/serviceValidate", :logout_url => "/cas/logout", :ssl => true - when "myusa" - config.omniauth :myusa, - ApiUmbrellaConfig[:web][:admin][:auth_strategies][:myusa][:client_id], - ApiUmbrellaConfig[:web][:admin][:auth_strategies][:myusa][:client_secret], - :scope => "profile.email" when "persona" + require "omniauth-persona" config.omniauth :persona else raise "Unknown authentication strategy enabled in config: #{strategy.inspect}" @@ -288,7 +303,7 @@ # # config.warden do |manager| # manager.intercept_401 = false - # manager.default_strategies(:scope => :user).unshift :some_external_strategy + # manager.default_strategies(scope: :user).unshift :some_external_strategy # end # ==> Mountable engine configurations @@ -301,7 +316,41 @@ # The router that invoked `devise_for`, in the example above, would be: # config.router_name = :my_engine # - # When using omniauth, Devise cannot automatically set Omniauth path, + # When using OmniAuth, Devise cannot automatically set OmniAuth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' end + +# In the test environment, allow the OmniAuth data to be mocked via a special +# "test_mock_omniauth" cookie. This gives us a way to mock the OmniAuth +# responses from our external integration tests. +if(Rails.env.test?) + # Define a middleware that looks for the special "test_mock_omniauth" cookie + # and sets up the omniauth mock data based on its value. + class TestMockOmniauth + def initialize(app) + @app = app + end + + def call(env) + begin + request = ActionDispatch::Request.new(env) + if(request.cookies["test_mock_omniauth"].present?) + data = MultiJson.load(Base64.urlsafe_decode64(request.cookies["test_mock_omniauth"])) + OmniAuth.config.test_mode = true + OmniAuth.config.add_mock(data["provider"], data) + else + OmniAuth.config.test_mode = false + end + + status, headers, body = @app.call(env) + ensure + OmniAuth.config.test_mode = false + end + + [status, headers, body] + end + end + + Rails.application.config.middleware.insert_after(Warden::Manager, TestMockOmniauth) +end diff --git a/src/api-umbrella/web-app/config/initializers/filter_parameter_logging.rb b/src/api-umbrella/web-app/config/initializers/filter_parameter_logging.rb new file mode 100644 index 000000000..4a994e1e7 --- /dev/null +++ b/src/api-umbrella/web-app/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/src/api-umbrella/web-app/config/initializers/inflections.rb b/src/api-umbrella/web-app/config/initializers/inflections.rb index 5d8d9be23..ac033bf9d 100644 --- a/src/api-umbrella/web-app/config/initializers/inflections.rb +++ b/src/api-umbrella/web-app/config/initializers/inflections.rb @@ -1,15 +1,16 @@ # Be sure to restart your server when you modify this file. -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end -# + # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections do |inflect| +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end diff --git a/src/api-umbrella/web-app/config/initializers/json.rb b/src/api-umbrella/web-app/config/initializers/json.rb index a5b65dfcc..0a57e22b4 100644 --- a/src/api-umbrella/web-app/config/initializers/json.rb +++ b/src/api-umbrella/web-app/config/initializers/json.rb @@ -1,5 +1,8 @@ -# Fix json issues with serializing times: -# https://github.com/ohler55/oj/issues/192 +# Don't include milliseconds on timestamps to maintain our older response +# formats. +ActiveSupport::JSON::Encoding.time_precision = 0 + Oj.default_options = { - :use_to_json => true + # Integrate Oj with the to_json methods Rails adds to objects. + :use_to_json => true, } diff --git a/src/api-umbrella/web-app/config/initializers/mime_types.rb b/src/api-umbrella/web-app/config/initializers/mime_types.rb index 72aca7e44..dc1899682 100644 --- a/src/api-umbrella/web-app/config/initializers/mime_types.rb +++ b/src/api-umbrella/web-app/config/initializers/mime_types.rb @@ -2,4 +2,3 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone diff --git a/src/api-umbrella/web-app/config/initializers/mongoid_none_scope.rb b/src/api-umbrella/web-app/config/initializers/mongoid_none_scope.rb deleted file mode 100644 index 082b7031d..000000000 --- a/src/api-umbrella/web-app/config/initializers/mongoid_none_scope.rb +++ /dev/null @@ -1,72 +0,0 @@ -# Backport "none" scope to Mongoid 3: -# https://gist.github.com/backspace/d77d93c892da8c2577f9 -if(Mongoid::VERSION.to_i != 3) - STDERR.puts "WARNING: Mongoid no longer version 3. config/initializers/mongoid_none_scope.rb should no longer be needed in Mongoid 4" -else - # rubocop:disable all - module Mongoid - class Criteria - def none - @none = true and self - end - - def empty_and_chainable? - !!@none - end - end - - module Contextual - class None - include ::Enumerable - - # Previously included Queryable, which has been extracted in v4 - attr_reader :collection, :criteria, :klass - - def blank? - !exists? - end - alias :empty? :blank? - - attr_reader :criteria, :klass - - def ==(other) - other.is_a?(None) - end - - def each - if block_given? - [].each { |doc| yield(doc) } - self - else - to_enum - end - end - - def exists?; false; end - - def initialize(criteria) - @criteria, @klass = criteria, criteria.klass - end - - def last; nil; end - - def length - entries.length - end - alias :size :length - end - - private - - def create_context - return None.new(self) if empty_and_chainable? - embedded ? Memory.new(self) : Mongo.new(self) - end - end - - module Finders - delegate :none, to: :with_default_scope - end - end - # rubocop:enable all -end diff --git a/src/api-umbrella/web-app/config/initializers/rollbar.rb b/src/api-umbrella/web-app/config/initializers/rollbar.rb index 8b3518729..bda8bcb0b 100644 --- a/src/api-umbrella/web-app/config/initializers/rollbar.rb +++ b/src/api-umbrella/web-app/config/initializers/rollbar.rb @@ -1,5 +1,7 @@ if(ApiUmbrellaConfig[:rollbar] && ApiUmbrellaConfig[:rollbar][:web_token].present?) - require 'rollbar' + require "rollbar" + require "rack/timeout/rollbar" + Rollbar.configure do |config| # Without configuration, Rollbar is enabled in all environments. # To disable in specific environments, set config.enabled=false. @@ -52,5 +54,12 @@ # config.use_sidekiq # You can supply custom Sidekiq options: # config.use_sidekiq 'queue' => 'default' + + # If you run your staging application instance in production environment then + # you'll want to override the environment reported by `Rails.env` with an + # environment variable like this: `ROLLBAR_ENV=staging`. This is a recommended + # setup for Heroku. See: + # https://devcenter.heroku.com/articles/deploying-to-a-custom-rails-environment + config.environment = ENV['ROLLBAR_ENV'] || Rails.env end end diff --git a/src/api-umbrella/web-app/config/initializers/secret_token.rb b/src/api-umbrella/web-app/config/initializers/secret_token.rb deleted file mode 100644 index d67b5a4c0..000000000 --- a/src/api-umbrella/web-app/config/initializers/secret_token.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -development_secret_token = "27f8bb481eebc6a51a88b10582798a5a8ce187ebd4a1e6882493ee3590fe53e01367133d8e2f8806dc8cee6436180460c54862cf3bc72f43eca515c885d29777" -ApiUmbrella::Application.config.secret_token = ENV["RAILS_SECRET_TOKEN"] || ApiUmbrellaConfig[:web][:rails_secret_token] || development_secret_token -if(!%w(development test).include?(Rails.env)) - if(ApiUmbrella::Application.config.secret_token == development_secret_token) - raise "An insecure secret token is being used. Please set the RAILS_SECRET_TOKEN environment variable with your own private key. Run 'rake secret_keys:generate' for more details." - end -end diff --git a/src/api-umbrella/web-app/config/initializers/session_store.rb b/src/api-umbrella/web-app/config/initializers/session_store.rb index 593ae4cfe..c81554dbe 100644 --- a/src/api-umbrella/web-app/config/initializers/session_store.rb +++ b/src/api-umbrella/web-app/config/initializers/session_store.rb @@ -1,6 +1,6 @@ # Be sure to restart your server when you modify this file. -ApiUmbrella::Application.config.session_store(:cookie_store, { +Rails.application.config.session_store(:cookie_store, { :key => "_api_umbrella_session", # Don't allow cookies to be accessed by javascript. @@ -9,8 +9,3 @@ # Use secure cookies to prevent sidejacking. :secure => !["development", "test"].include?(Rails.env), }) - -# Use the database for sessions instead of the cookie-based default, -# which shouldn't be used to store highly confidential information -# (create the session table with "rails generate session_migration") -# ApiUmbrella::Application.config.session_store :active_record_store diff --git a/src/api-umbrella/web-app/config/initializers/sprockets.rb b/src/api-umbrella/web-app/config/initializers/sprockets.rb deleted file mode 100644 index b8c6756cd..000000000 --- a/src/api-umbrella/web-app/config/initializers/sprockets.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Fix relative CSS urls to absolute ones during precompile. -if(!Rails.application.config.serve_static_assets && Rails.groups.include?("assets")) - Rails.application.assets.register_preprocessor 'text/css', Sprockets::UrlRewriter -end diff --git a/src/api-umbrella/web-app/config/initializers/tabs_on_rails.rb b/src/api-umbrella/web-app/config/initializers/tabs_on_rails.rb deleted file mode 100644 index 69cee4250..000000000 --- a/src/api-umbrella/web-app/config/initializers/tabs_on_rails.rb +++ /dev/null @@ -1,8 +0,0 @@ -class TabsOnRails::Tabs::TabsBuilder - def tab_for(tab, name, url_options, item_options = {}) - item_options[:class] = item_options[:class].to_s.split(" ").push("current").join(" ") if current_tab?(tab) - link_html_options = item_options.delete(:link_html) - content = @context.link_to(name, url_options, link_html_options) - @context.content_tag(:li, content, item_options) - end -end diff --git a/src/api-umbrella/web-app/config/initializers/timeout.rb b/src/api-umbrella/web-app/config/initializers/timeout.rb index e0c73c013..36403d10b 100644 --- a/src/api-umbrella/web-app/config/initializers/timeout.rb +++ b/src/api-umbrella/web-app/config/initializers/timeout.rb @@ -3,3 +3,6 @@ else Rack::Timeout.timeout = 15 # seconds end + +Rack::Timeout::Logger.device = $stderr +Rack::Timeout::Logger.level = Logger::ERROR diff --git a/src/api-umbrella/web-app/config/initializers/userstamp.rb b/src/api-umbrella/web-app/config/initializers/userstamp.rb index dc2fff947..f04ffdad2 100644 --- a/src/api-umbrella/web-app/config/initializers/userstamp.rb +++ b/src/api-umbrella/web-app/config/initializers/userstamp.rb @@ -1,7 +1,29 @@ - Mongoid::Userstamp.config do |c| - c.user_reader = :current_admin - c.user_model = :admin +module Mongoid + module Userstamp + extend ActiveSupport::Concern - c.updated_column_opts = { :type => String } - c.created_column_opts = { :type => String } - end + included do + belongs_to :creator, :class_name => "Admin", :foreign_key => :created_by + belongs_to :updater, :class_name => "Admin", :foreign_key => :updated_by + + before_create :set_created_by + before_save :set_updated_by + + protected + + def set_created_by + current_user = RequestStore.store[:current_userstamp_user] + if(current_user && !self.created_by_changed?) + self.created_by = current_user.id + end + end + + def set_updated_by + current_user = RequestStore.store[:current_userstamp_user] + if(current_user && !self.updated_by_changed?) + self.updated_by = current_user.id + end + end + end + end +end diff --git a/src/api-umbrella/web-app/config/initializers/wrap_parameters.rb b/src/api-umbrella/web-app/config/initializers/wrap_parameters.rb new file mode 100644 index 000000000..553c789b7 --- /dev/null +++ b/src/api-umbrella/web-app/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters :format => [:json] if respond_to?(:wrap_parameters) +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/src/api-umbrella/web-app/config/initializers/x_forwarded_for_trusted_proxies_fix.rb b/src/api-umbrella/web-app/config/initializers/x_forwarded_for_trusted_proxies_fix.rb deleted file mode 100644 index 664380b15..000000000 --- a/src/api-umbrella/web-app/config/initializers/x_forwarded_for_trusted_proxies_fix.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActionDispatch - class RemoteIp - # Override the default list of trusted_proxies to only include 127.0.0.1. - # - # Without this, Rails methods like request.remote_ip and request.local? get - # things wrong with the combination of X-Forwarded-For headers (coming from - # HAProxy) and our internal network IP addresses. The problem is that our - # internal IP addresses start with 10.*. By default, Rails considers IPs in - # this range to be local requests. However, we don't actaully want these - # IPs considered local when we're on our staging or production servers, - # even if the requests are coming from internal NREL computers. Otherwise, - # Rails will return debug error messages to AppScan and Cyber Security, as - # well as remote_ip being wrong. - # - # Ideally this could be configured using only the - # `config.action_dispatch.trusted_proxies` app config, but currently that - # configuration parameter is only additive (it can't get rid of the default - # 10.* addresses): https://github.com/rails/rails/pull/2632 - def initialize(app, check_ip_spoofing = true, trusted_proxies = nil) - @app = app - @check_ip_spoofing = check_ip_spoofing - regex = '(^127\.0\.0\.1$)' - regex << "|(#{trusted_proxies})" if trusted_proxies - @trusted_proxies = Regexp.new(regex, "i") - end - end -end diff --git a/src/api-umbrella/web-app/config/locales/de.yml b/src/api-umbrella/web-app/config/locales/de.yml index 67e7d4f79..5a44f4397 100644 --- a/src/api-umbrella/web-app/config/locales/de.yml +++ b/src/api-umbrella/web-app/config/locales/de.yml @@ -7,7 +7,6 @@ de: facebook: Facebook github: GitHub google_oauth2: Google - myusa: MyUSA persona: Persona errors: messages: @@ -95,4 +94,4 @@ de: add: "Add Rewrite" sub_settings: legend: "Sub-URL Request Settings" - note: "Change settings for specific sub-URLs within this API." \ No newline at end of file + note: "Change settings for specific sub-URLs within this API." diff --git a/src/api-umbrella/web-app/config/locales/devise.en.yml b/src/api-umbrella/web-app/config/locales/devise.en.yml index 6cd4cd277..bd4c3ebc6 100644 --- a/src/api-umbrella/web-app/config/locales/devise.en.yml +++ b/src/api-umbrella/web-app/config/locales/devise.en.yml @@ -3,49 +3,52 @@ en: devise: confirmations: - confirmed: "Your account was successfully confirmed." - send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes." + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." failure: already_authenticated: "You are already signed in." inactive: "Your account is not activated yet." - invalid: "Invalid email or password." + invalid: "Invalid %{authentication_keys} or password." locked: "Your account is locked." - last_attempt: "You have one more attempt before your account will be locked." - not_found_in_database: "Invalid email or password." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." - unconfirmed: "You have to confirm your account before continuing." + unconfirmed: "You have to confirm your email address before continuing." mailer: confirmation_instructions: subject: "Confirmation instructions" reset_password_instructions: subject: "Reset password instructions" unlock_instructions: - subject: "Unlock Instructions" + subject: "Unlock instructions" + password_change: + subject: "Password Changed" omniauth_callbacks: failure: "Could not authenticate you from %{kind} because \"%{reason}\"." success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - send_instructions: "You will receive an email with instructions about how to reset your password in a few minutes." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." - updated: "Your password was changed successfully. You are now signed in." - updated_not_active: "Your password was changed successfully." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." registrations: - destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon." + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." signed_up: "Welcome! You have signed up successfully." signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." - signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." - update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." - updated: "You updated your account successfully." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." sessions: signed_in: "Signed in successfully." signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." unlocks: - send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes." - send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes." + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: messages: diff --git a/src/api-umbrella/web-app/config/locales/en.yml b/src/api-umbrella/web-app/config/locales/en.yml index 943787e7a..b6c945fdc 100644 --- a/src/api-umbrella/web-app/config/locales/en.yml +++ b/src/api-umbrella/web-app/config/locales/en.yml @@ -6,7 +6,6 @@ en: facebook: Facebook github: GitHub google_oauth2: Google - myusa: MyUSA persona: Persona ldap: LDAP errors: diff --git a/src/api-umbrella/web-app/config/locales/es-419.yml b/src/api-umbrella/web-app/config/locales/es-419.yml index 69ed9afec..f21ee954a 100644 --- a/src/api-umbrella/web-app/config/locales/es-419.yml +++ b/src/api-umbrella/web-app/config/locales/es-419.yml @@ -7,7 +7,6 @@ es-419: facebook: Facebook github: GitHub google_oauth2: Google - myusa: MyUSA persona: Persona errors: messages: @@ -95,4 +94,4 @@ es-419: add: "Add Rewrite" sub_settings: legend: "Sub-URL Request Settings" - note: "Change settings for specific sub-URLs within this API." \ No newline at end of file + note: "Change settings for specific sub-URLs within this API." diff --git a/src/api-umbrella/web-app/config/locales/fi.yml b/src/api-umbrella/web-app/config/locales/fi.yml index 24b5f9425..8aed97d4b 100644 --- a/src/api-umbrella/web-app/config/locales/fi.yml +++ b/src/api-umbrella/web-app/config/locales/fi.yml @@ -7,7 +7,6 @@ fi: facebook: Facebook github: GitHub google_oauth2: Google - myusa: MyUSA persona: Persona errors: messages: @@ -95,4 +94,4 @@ fi: add: "Uusi rewrite -sääntö" sub_settings: legend: "Ali URL Pyyntiasetukset" - note: "Muuta tietyn sub-URL:ienn asetuksia tässä API:ssa." \ No newline at end of file + note: "Muuta tietyn sub-URL:ienn asetuksia tässä API:ssa." diff --git a/src/api-umbrella/web-app/config/locales/fr.yml b/src/api-umbrella/web-app/config/locales/fr.yml index 764811268..e86702d59 100644 --- a/src/api-umbrella/web-app/config/locales/fr.yml +++ b/src/api-umbrella/web-app/config/locales/fr.yml @@ -7,7 +7,6 @@ fr: facebook: Facebook github: GitHub google_oauth2: Google - myusa: MyUSA persona: Persona errors: messages: @@ -95,4 +94,4 @@ fr: add: "Ajouter une règle" sub_settings: legend: "Réécriture des composants d'URL" - note: "Modifier les paramètres de réécriture des composants d'URL." \ No newline at end of file + note: "Modifier les paramètres de réécriture des composants d'URL." diff --git a/src/api-umbrella/web-app/config/locales/it.yml b/src/api-umbrella/web-app/config/locales/it.yml index 29e5b6798..e7ef1bd0b 100644 --- a/src/api-umbrella/web-app/config/locales/it.yml +++ b/src/api-umbrella/web-app/config/locales/it.yml @@ -7,7 +7,6 @@ it: facebook: Facebook github: GitHub google_oauth2: Google - myusa: MyUSA persona: Persona errors: messages: @@ -95,4 +94,4 @@ it: add: "Aggiungi Riscrittura" sub_settings: legend: "Opzioni Richiesta Sotto-URL" - note: "Cambia le opzioni per spefici sotto-URL dentro questa API." \ No newline at end of file + note: "Cambia le opzioni per spefici sotto-URL dentro questa API." diff --git a/src/api-umbrella/web-app/config/locales/ru.yml b/src/api-umbrella/web-app/config/locales/ru.yml index 429882f49..339a661c3 100644 --- a/src/api-umbrella/web-app/config/locales/ru.yml +++ b/src/api-umbrella/web-app/config/locales/ru.yml @@ -7,7 +7,6 @@ ru: facebook: Facebook github: GitHub google_oauth2: Google - myusa: MyUSA persona: Persona errors: messages: @@ -95,4 +94,4 @@ ru: add: "Добавить перезапись" sub_settings: legend: "Запрос sub-URL настроек" - note: "Изменить настройки для определенных sub-URL в рамке этого API." \ No newline at end of file + note: "Изменить настройки для определенных sub-URL в рамке этого API." diff --git a/src/api-umbrella/web-app/config/locales/zy.yml b/src/api-umbrella/web-app/config/locales/zy.yml new file mode 100644 index 000000000..1fcf8f72f --- /dev/null +++ b/src/api-umbrella/web-app/config/locales/zy.yml @@ -0,0 +1,5 @@ +# A purposefully invalid/incomplete locale file for testing against (to check +# the fallback behavior of other locales that might be incomplete). +zy: + omniauth_providers: + facebook: Facebook diff --git a/src/api-umbrella/web-app/config/mongoid.yml b/src/api-umbrella/web-app/config/mongoid.yml deleted file mode 100644 index 3e6acce7b..000000000 --- a/src/api-umbrella/web-app/config/mongoid.yml +++ /dev/null @@ -1,31 +0,0 @@ -defaults: &defaults - sessions: - default: - uri: <%= ApiUmbrellaConfig[:mongodb][:url] %> - options: - safe: true - options: - allow_dynamic_fields: false - -development: - <<: *defaults - -test: - sessions: - default: - uri: <%= ApiUmbrellaConfig[:mongodb][:url] %> - options: - safe: true - consistency: :strong - # In the test environment we lower the retries and retry interval to - # low amounts for fast failures. - max_retries: 1 - retry_interval: 0 - options: - allow_dynamic_fields: false - -staging: - <<: *defaults - -production: - <<: *defaults diff --git a/src/api-umbrella/web-app/config/routes.rb b/src/api-umbrella/web-app/config/routes.rb index 94b7dadec..aa47190e9 100644 --- a/src/api-umbrella/web-app/config/routes.rb +++ b/src/api-umbrella/web-app/config/routes.rb @@ -1,6 +1,9 @@ require "api_umbrella/elasticsearch_proxy" -ApiUmbrella::Application.routes.draw do +Rails.application.routes.draw do + # Add a simple health-check endpoint to see if this app is up. + get "/_web-app-health", :to => proc { [200, {}, ["OK"]] } + # Mount the API at both /api/ and /api-umbrella/ for backwards compatibility. %w(api api-umbrella).each do |path| namespace(:api, :path => path) do @@ -37,7 +40,7 @@ put "move_after" end end - resources :users + resources :users, :except => [:destroy] resources :website_backends resource :contact, :only => [:create] @@ -59,11 +62,10 @@ devise_scope :admin do get "/admin/login" => "admin/sessions#new", :as => :new_admin_session - get "/admin/logout" => "admin/sessions#destroy", :as => :destroy_admin_session + delete "/admin/logout" => "admin/sessions#destroy", :as => :destroy_admin_session + get "/admin/auth" => "admin/sessions#auth" end - match "/admin" => "admin/base#empty" - namespace :admin do resources :stats, :only => [:index] do collection do @@ -80,7 +82,34 @@ end end - authenticate :admin, lambda { |admin| admin.superuser? } do - mount ApiUmbrella::ElasticsearchProxy.new, :at => ApiUmbrella::ElasticsearchProxy::PREFIX + authenticate :admin do + mount ApiUmbrella::ElasticsearchProxy.new => ApiUmbrella::ElasticsearchProxy::PREFIX end + + # Add an endpoint for admin-ui to hit to return the detected language based + # on the Accept-Language HTTP header. + # + # At some point we may want to revisit this to be purely client-side, but + # this currently seems like the easiest approach to ensure that the parsed + # client-side language is consistent with the server-side language, and this + # can be tested with Capybara (purely client-side approaches based on + # "navigator.languages" can't really seem to be changed in + # Capybara+poltergeist). + get "/admin/i18n_detection.js", :to => proc { |env| + locale = env["http_accept_language.parser"].compatible_language_from(I18n.available_locales) || I18n.default_locale + + [ + 200, + { + "Content-Type" => "application/javascript", + "Cache-Control" => "max-age=0, private, must-revalidate", + }, + [ + "I18n = {};", + "I18n.defaultLocale = #{I18n.default_locale.to_json};", + "I18n.locale = #{locale.to_json};", + "I18n.fallbacks = true;", + ], + ] + } end diff --git a/src/api-umbrella/web-app/config/secrets.yml b/src/api-umbrella/web-app/config/secrets.yml new file mode 100644 index 000000000..0ffae1256 --- /dev/null +++ b/src/api-umbrella/web-app/config/secrets.yml @@ -0,0 +1,22 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rake secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +development: + secret_key_base: 9362d7f88f100d3424229b6c54ff776734927bf3ce1bd67134174d3474b647c8b6e9cd046478bdaa3d640bfad1cd280535fc1198233a6808ce42a0e42ea4b2ea + +test: + secret_key_base: aeec385fb48a0594b6bb0b18f62473190f1d01b0b6113766af525be2ae1a317a03ab0ee1b3ee6aca3fb1572dc87684e033dcec21acd90d0ca0f111ca1785d0e9 + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] || ApiUmbrellaConfig[:web][:rails_secret_token] %> diff --git a/src/api-umbrella/web-app/db/migrate/20131020035658_user_uuids.rb b/src/api-umbrella/web-app/db/migrate/20131020035658_user_uuids.rb index 3b9b2dfd0..58b709473 100644 --- a/src/api-umbrella/web-app/db/migrate/20131020035658_user_uuids.rb +++ b/src/api-umbrella/web-app/db/migrate/20131020035658_user_uuids.rb @@ -14,7 +14,7 @@ def self.up # UUID _id value. new_user = user.clone new_user.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(user._id)) - new_user._id = UUIDTools::UUID.random_create.to_s + new_user._id = SecureRandom.uuid puts "#{user._id} => #{new_user._id}" diff --git a/src/api-umbrella/web-app/db/migrate/20131107231237_api_uuids.rb b/src/api-umbrella/web-app/db/migrate/20131107231237_api_uuids.rb index 36a5816c7..bb3f3c6b7 100644 --- a/src/api-umbrella/web-app/db/migrate/20131107231237_api_uuids.rb +++ b/src/api-umbrella/web-app/db/migrate/20131107231237_api_uuids.rb @@ -14,16 +14,16 @@ def self.up # UUID _id value. new_api = api.clone new_api.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(api._id)) - new_api._id = UUIDTools::UUID.random_create.to_s + new_api._id = SecureRandom.uuid if(new_api.settings) new_api.settings.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(new_api.settings._id)) - new_api.settings._id = UUIDTools::UUID.random_create.to_s + new_api.settings._id = SecureRandom.uuid if(new_api.settings.headers) new_api.settings.headers.each do |header| header.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(header._id)) - header._id = UUIDTools::UUID.random_create.to_s + header._id = SecureRandom.uuid end end end @@ -31,30 +31,30 @@ def self.up if(new_api.servers) new_api.servers.each do |server| server.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(server._id)) - server._id = UUIDTools::UUID.random_create.to_s + server._id = SecureRandom.uuid end end if(new_api.url_matches) new_api.url_matches.each do |url_match| url_match.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(url_match._id)) - url_match._id = UUIDTools::UUID.random_create.to_s + url_match._id = SecureRandom.uuid end end if(new_api.sub_settings) new_api.sub_settings.each do |sub_setting| sub_setting.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(sub_setting._id)) - sub_setting._id = UUIDTools::UUID.random_create.to_s + sub_setting._id = SecureRandom.uuid if(sub_setting.settings) sub_setting.settings.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(sub_setting.settings._id)) - sub_setting.settings._id = UUIDTools::UUID.random_create.to_s + sub_setting.settings._id = SecureRandom.uuid if(sub_setting.settings.headers) sub_setting.settings.headers.each do |header| header.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(header._id)) - header._id = UUIDTools::UUID.random_create.to_s + header._id = SecureRandom.uuid end end end @@ -64,14 +64,14 @@ def self.up if(new_api.rewrites) new_api.rewrites.each do |rewrite| rewrite.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(rewrite._id)) - rewrite._id = UUIDTools::UUID.random_create.to_s + rewrite._id = SecureRandom.uuid end end if(new_api.read_attribute(:rate_limits)) new_api.read_attribute(:rate_limits).each do |limit| limit.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(limit._id)) - limit._id = UUIDTools::UUID.random_create.to_s + limit._id = SecureRandom.uuid end end diff --git a/src/api-umbrella/web-app/db/migrate/20131107233429_admin_uuids.rb b/src/api-umbrella/web-app/db/migrate/20131107233429_admin_uuids.rb index dde571c10..d02bdab31 100644 --- a/src/api-umbrella/web-app/db/migrate/20131107233429_admin_uuids.rb +++ b/src/api-umbrella/web-app/db/migrate/20131107233429_admin_uuids.rb @@ -14,7 +14,7 @@ def self.up # UUID _id value. new_admin = admin.clone new_admin.write_attribute(:legacy_id, Moped::BSON::ObjectId.from_string(admin._id)) - new_admin._id = UUIDTools::UUID.random_create.to_s + new_admin._id = SecureRandom.uuid puts "#{admin._id} => #{new_admin._id}" diff --git a/src/api-umbrella/web-app/db/migrate/20160229172850_move_cached_city_geocodes_to_mongo.rb b/src/api-umbrella/web-app/db/migrate/20160229172850_move_cached_city_geocodes_to_mongo.rb index 13d60db66..22fd0051e 100644 --- a/src/api-umbrella/web-app/db/migrate/20160229172850_move_cached_city_geocodes_to_mongo.rb +++ b/src/api-umbrella/web-app/db/migrate/20160229172850_move_cached_city_geocodes_to_mongo.rb @@ -2,7 +2,7 @@ class MoveCachedCityGeocodesToMongo < Mongoid::Migration def self.up client = Elasticsearch::Client.new({ :hosts => ApiUmbrellaConfig[:elasticsearch][:hosts], - :logger => Rails.logger + :logger => Rails.logger, }) result = client.search({ @@ -27,7 +27,7 @@ def self.up hit["_source"]["location"]["lat"], ], }, - :updated_at => Time.at(hit["_source"]["updated_at"] / 1000.0), + :updated_at => Time.at(hit["_source"]["updated_at"] / 1000.0).utc, }) end end diff --git a/src/api-umbrella/web-app/db/seeds.rb b/src/api-umbrella/web-app/db/seeds.rb index f4b5ba555..54132ce6e 100644 --- a/src/api-umbrella/web-app/db/seeds.rb +++ b/src/api-umbrella/web-app/db/seeds.rb @@ -1,2 +1,2 @@ -# seed-fu is being used -# See db/fixtures and rake db:seed_fu instead. +# Seed with seed-fu +SeedFu.seed diff --git a/src/api-umbrella/web-app/lib/api_umbrella/attributify_data.rb b/src/api-umbrella/web-app/lib/api_umbrella/attributify_data.rb index c92c946a6..aea49bc6e 100644 --- a/src/api-umbrella/web-app/lib/api_umbrella/attributify_data.rb +++ b/src/api-umbrella/web-app/lib/api_umbrella/attributify_data.rb @@ -24,14 +24,19 @@ module AttributifyData # we're leveraging it in the ApiUser model to handle the similar nested # settings and rate limits. If those use cases start to diverge, # this should be revisited to make more abstract. - def assign_nested_attributes(data, options = {}) + def assign_nested_attributes(data) if(data.kind_of?(Hash)) + if(!data.permitted?) + raise ActiveModel::ForbiddenAttributesError + end + data = data.deep_dup old_data = self.attributes attributify_data!(data, old_data) - self.assign_attributes(data, options) + data.permit! + self.assign_attributes(data) end end @@ -40,8 +45,10 @@ def assign_nested_attributes(data, options = {}) def attributify_data!(data, old_data) attributify_settings!(data, old_data) - %w(servers url_matches sub_settings rewrites).each do |collection_name| - attributify_embeds_many!(data, collection_name, old_data) + if(self.class == ::Api) + %w(servers url_matches sub_settings rewrites).each do |collection_name| + attributify_embeds_many!(data, collection_name, old_data) + end end end @@ -51,8 +58,24 @@ def attributify_settings!(data, old_data) settings_data = data["settings_attributes"] old_settings_data = old_data["settings"] if(old_data.present?) - %w(headers rate_limits default_response_headers override_response_headers).each do |collection_name| - attributify_embeds_many!(settings_data, collection_name, old_settings_data) + attributify_embeds_many!(settings_data, "rate_limits", old_settings_data) + if(self.class == ::Api) + %w(headers default_response_headers override_response_headers).each do |collection_name| + # The header associations are a bit different, since they accept + # either an array of nested attributes (like other nested object + # types), or a new-line delimited string. However, both cannot be set + # at the same time, or else Mongoid doesn't save properly (due to the + # string writer overwriting the old data ahead of when the nested + # object setter expects). So ensure only one of these is set. + object_key = collection_name + string_key = "#{collection_name}_string" + if(settings_data[string_key].present?) + settings_data.delete(object_key) + else + settings_data.delete(string_key) + attributify_embeds_many!(settings_data, collection_name, old_settings_data) + end + end end end diff --git a/src/api-umbrella/web-app/lib/api_umbrella/elasticsearch_proxy.rb b/src/api-umbrella/web-app/lib/api_umbrella/elasticsearch_proxy.rb index 18c902d5f..3d5d89195 100644 --- a/src/api-umbrella/web-app/lib/api_umbrella/elasticsearch_proxy.rb +++ b/src/api-umbrella/web-app/lib/api_umbrella/elasticsearch_proxy.rb @@ -1,6 +1,6 @@ module ApiUmbrella class ElasticsearchProxy < Rack::Proxy - PREFIX = "/admin/elasticsearch" + PREFIX = "/admin/elasticsearch".freeze def initialize(options = {}) super(options.merge({ @@ -8,6 +8,15 @@ def initialize(options = {}) })) end + def perform_request(env) + admin = env["warden"].user(:admin) + if(admin && admin.superuser?) + super + else + [403, {}, ["Forbidden"]] + end + end + def rewrite_env(env) # Rewrite /admin/elasticsearch to / %w(SCRIPT_NAME REQUEST_PATH REQUEST_URI).each do |key| @@ -28,9 +37,15 @@ def rewrite_env(env) def rewrite_response(triplet) status, headers, body = triplet - # Fix tag redirects returned by elasticsearch to add our URL - # custom prefix. + # Rewrite redirects if(status >= 300 && status < 400) + # Rewrite Location header redirects + url = [headers["location"]].flatten.join("") + if(url && !url.empty? && url.start_with?("/") && !url.start_with?(PREFIX)) + headers["location"] = File.join(PREFIX, url) + end + + # Rewrite tag redirects. new_body = body.to_s new_body.gsub!(/(url=['"]?)([^'">]+)/i) do tag = Regexp.last_match[1] diff --git a/src/api-umbrella/web-app/lib/common_validations.rb b/src/api-umbrella/web-app/lib/common_validations.rb index cd4e837b8..267f11cbe 100644 --- a/src/api-umbrella/web-app/lib/common_validations.rb +++ b/src/api-umbrella/web-app/lib/common_validations.rb @@ -1,6 +1,10 @@ module CommonValidations BASE_HOST_FORMAT = %r{[a-zA-Z0-9:][a-zA-Z0-9\-\.:]*} - HOST_FORMAT = %r{^#{BASE_HOST_FORMAT.source}$} - HOST_FORMAT_WITH_WILDCARD = %r{^(\*|(\*\.|\.)#{BASE_HOST_FORMAT.source}|#{BASE_HOST_FORMAT.source})$} - URL_PREFIX_FORMAT = %r{^/} + HOST_FORMAT = %r{\A#{BASE_HOST_FORMAT.source}\z} + HOST_FORMAT_WITH_WILDCARD = %r{\A(\*|(\*\.|\.)#{BASE_HOST_FORMAT.source}|#{BASE_HOST_FORMAT.source})\z} + URL_PREFIX_FORMAT = %r{\A/} + + def self.to_js(regex) + regex.source.gsub(/\A\\A/, "^").gsub(/\\z\z/, "$") + end end diff --git a/src/api-umbrella/web-app/lib/js_locale_helper.rb b/src/api-umbrella/web-app/lib/js_locale_helper.rb index d309ac635..c61fae38e 100644 --- a/src/api-umbrella/web-app/lib/js_locale_helper.rb +++ b/src/api-umbrella/web-app/lib/js_locale_helper.rb @@ -15,8 +15,6 @@ def self.output_locale(locale) result end - private - def self.markdown!(data) if(data.kind_of?(Hash)) data.each do |key, value| diff --git a/src/api-umbrella/web-app/lib/mail_sanitizer.rb b/src/api-umbrella/web-app/lib/mail_sanitizer.rb deleted file mode 100644 index 41baaad39..000000000 --- a/src/api-umbrella/web-app/lib/mail_sanitizer.rb +++ /dev/null @@ -1,32 +0,0 @@ -class MailSanitizer - class InvalidAddress < StandardError - end - - # A workaround to address OSVDB-131677 that is patched in the mail 2.6 gem, - # but since we're still on Rails 3.2, we can't upgrade yet. - # - # If the fixes get backported (https://github.com/mikel/mail/issues/944), - # then we could get rid of this, but in the meantime, this is a quick fix to - # address the underlying issues related to newlines and lengths. - # - # See: - # http://rubysec.com/advisories/OSVDB-131677/ - # http://www.mbsd.jp/Whitepaper/smtpi.pdf - def self.sanitize_address(address) - if(address) - # Ensure no linebreaks are in the address. - if(address =~ /[\r\n]/) - raise InvalidAddress, "E-mail address cannot contain newlines" - end - - # Ensure the address doesn't exceed 500 chars to prevent some servers - # from wrapping the content, introducing line breaks (technically, longer - # should work, but 500 seems like enough for our simple purposes). - if(address.length > 500) - raise InvalidAddress, "E-mail address cannot exceed 500 characters" - end - end - - address - end -end diff --git a/src/api-umbrella/web-app/lib/symbolize_helper.rb b/src/api-umbrella/web-app/lib/symbolize_helper.rb deleted file mode 100644 index eeefbc58f..000000000 --- a/src/api-umbrella/web-app/lib/symbolize_helper.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SymbolizeHelper - def self.symbolize_recursive(hash) - {}.tap do |h| - hash.each { |key, value| h[key.to_sym] = map_value(value) } - end - end - - def self.map_value(thing) - case thing - when Hash - symbolize_recursive(thing) - when Array - thing.map { |v| map_value(v) } - else - thing - end - end -end diff --git a/src/api-umbrella/web-app/lib/tasks/jshint.rake b/src/api-umbrella/web-app/lib/tasks/jshint.rake deleted file mode 100644 index 9bf2630d5..000000000 --- a/src/api-umbrella/web-app/lib/tasks/jshint.rake +++ /dev/null @@ -1,58 +0,0 @@ -begin - require "jshintrb/jshinttask" - Jshintrb::JshintTask.new :jshint do |t| - t.pattern = 'app/assets/javascripts/**/*.js' - t.exclude_pattern = [ - 'app/assets/javascripts/vendor/**/*.js', - 'app/assets/javascripts/polyfills/**/*.js', - ] - t.options = { - :browser => true, - :esnext => true, - :bitwise => true, - :camelcase => false, - :curly => true, - :eqeqeq => true, - :immed => true, - :indent => 2, - :latedef => true, - :newcap => true, - :noarg => true, - :quotmark => "single", - :regexp => true, - :sub => true, - :undef => true, - :unused => true, - :strict => false, - :trailing => true, - :smarttabs => true, - } - t.globals = [ - "_", - "$", - "ace", - "Admin", - "bootbox", - "CommonValidations", - "currentAdmin", - "Ember", - "enableBetaAnalytics", - "google", - "ic", - "inflection", - "JsDiff", - "jstz", - "jQuery", - "marked", - "moment", - "numeral", - "PNotify", - "polyglot", - "Spinner", - "webAdminAjaxApiKey", - ] - end -rescue LoadError - desc "You need the `jshintrb` gem to run jshint" - task :jshint -end diff --git a/src/api-umbrella/web-app/lib/tasks/secret_keys.rake b/src/api-umbrella/web-app/lib/tasks/secret_keys.rake deleted file mode 100644 index 257e03a59..000000000 --- a/src/api-umbrella/web-app/lib/tasks/secret_keys.rake +++ /dev/null @@ -1,18 +0,0 @@ -namespace :secret_keys do - desc "" - task :generate do - puts <<-eos - -Here are new, random keys you can use for running your application in -production: - - RAILS_SECRET_TOKEN=#{SecureRandom.hex(64)} - DEVISE_SECRET_KEY=#{SecureRandom.hex(64)} - -It's recommended that you store these as environment variables on your servers. -A local .env file inside this project may be used for this purpose. See the -dotenv gem for more details: https://github.com/bkeepers/dotenv - - eos - end -end diff --git a/src/api-umbrella/web-app/lib/tasks/yard.rake b/src/api-umbrella/web-app/lib/tasks/yard.rake deleted file mode 100644 index 915e85734..000000000 --- a/src/api-umbrella/web-app/lib/tasks/yard.rake +++ /dev/null @@ -1,8 +0,0 @@ -begin - require "yard" - - YARD::Rake::YardocTask.new -rescue LoadError - desc "You need the `yard` gem to generate documentation" - task :yard -end diff --git a/src/api-umbrella/web-app/lib/yard_template/default/fulldoc/html/css/common.css b/src/api-umbrella/web-app/lib/yard_template/default/fulldoc/html/css/common.css deleted file mode 100644 index afcc49432..000000000 --- a/src/api-umbrella/web-app/lib/yard_template/default/fulldoc/html/css/common.css +++ /dev/null @@ -1,4 +0,0 @@ -#filecontents li > p, .docstring li > p { - margin-top: 1em; - margin-bottom: 1em; -} diff --git a/src/api-umbrella/web-app/public/404.html b/src/api-umbrella/web-app/public/404.html deleted file mode 100644 index 9a48320a5..000000000 --- a/src/api-umbrella/web-app/public/404.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - -
        -

        The page you were looking for doesn't exist.

        -

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

        -
        - - diff --git a/src/api-umbrella/web-app/public/422.html b/src/api-umbrella/web-app/public/422.html deleted file mode 100644 index 83660ab18..000000000 --- a/src/api-umbrella/web-app/public/422.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - The change you wanted was rejected (422) - - - - - -
        -

        The change you wanted was rejected.

        -

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

        -
        - - diff --git a/src/api-umbrella/web-app/public/500.html b/src/api-umbrella/web-app/public/500.html deleted file mode 100644 index b80307fc1..000000000 --- a/src/api-umbrella/web-app/public/500.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - -
        -

        We're sorry, but something went wrong.

        -

        We've been notified about this issue and we'll take a look at it shortly.

        -
        - - diff --git a/src/api-umbrella/web-app/public/favicon.ico b/src/api-umbrella/web-app/public/favicon.ico deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/api-umbrella/web-app/script/migrate_logs b/src/api-umbrella/web-app/script/migrate_logs index 59e83c9d9..a40ee3da8 100755 --- a/src/api-umbrella/web-app/script/migrate_logs +++ b/src/api-umbrella/web-app/script/migrate_logs @@ -70,8 +70,8 @@ indices.each do |source_index| # rubocop:disable Lint/ShadowingOuterLocalVariabl scroll_id = result["_scroll_id"] total_hits = result["hits"]["total"] count = 0 - while(scroll = source_client.scroll(:scroll_id => scroll_id, :scroll => "10m")) # rubocop:disable Lint/LiteralInCondition - start_time = Time.now + while(scroll = source_client.scroll(:scroll_id => scroll_id, :scroll => "10m")) # rubocop:disable Lint/AssignmentInCondition + start_time = Time.now.utc scroll_id = scroll["_scroll_id"] hits = scroll["hits"]["hits"] @@ -120,7 +120,7 @@ indices.each do |source_index| # rubocop:disable Lint/ShadowingOuterLocalVariabl if(bulk_commands.any?) dest_client.bulk(:body => bulk_commands) - elapsed_time = Time.now - start_time + elapsed_time = Time.now.utc - start_time logger.info "Indexed #{count} of #{total_hits} (#{bulk_commands.length / 2} records indexed in #{elapsed_time.round} seconds)" end end diff --git a/src/api-umbrella/web-app/script/rails b/src/api-umbrella/web-app/script/rails deleted file mode 100755 index f8da2cffd..000000000 --- a/src/api-umbrella/web-app/script/rails +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -APP_PATH = File.expand_path('../../config/application', __FILE__) -require File.expand_path('../../config/boot', __FILE__) -require 'rails/commands' diff --git a/src/api-umbrella/web-app/spec/controllers/admin/admins/omniauth_callbacks_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/admin/admins/omniauth_callbacks_controller_spec.rb deleted file mode 100644 index 3ea2c8413..000000000 --- a/src/api-umbrella/web-app/spec/controllers/admin/admins/omniauth_callbacks_controller_spec.rb +++ /dev/null @@ -1,200 +0,0 @@ -require 'spec_helper' - -describe Admin::Admins::OmniauthCallbacksController do - before(:all) do - Admin.delete_all - @valid_admin = FactoryGirl.create(:admin, :username => "test@example.com") - @case_insensitive_admin = FactoryGirl.create(:admin, :username => "HELLO@example.com") - @unverified_admin = FactoryGirl.create(:admin, :username => "unverified@example.com") - end - - before(:each) do - OmniAuth.config.test_mode = true - OmniAuth.config.mock_auth[:cas] = nil - OmniAuth.config.mock_auth[:developer] = nil - OmniAuth.config.mock_auth[:facebook] = nil - OmniAuth.config.mock_auth[:github] = nil - OmniAuth.config.mock_auth[:google_oauth2] = nil - OmniAuth.config.mock_auth[:myusa] = nil - OmniAuth.config.mock_auth[:persona] = nil - OmniAuth.config.mock_auth[:twitter] = nil - - request.env["devise.mapping"] = Devise.mappings[:admin] - end - - shared_examples "omniauth login" do |provider| - it "allows valid admins" do - request.env["omniauth.auth"] = @valid_omniauth - - get(provider) - session["warden.user.admin.key"].should be_kind_of(Array) - session["warden.user.admin.key"][0].should be_kind_of(Array) - admin_id = session["warden.user.admin.key"][0][0] - admin_id.should eql(@valid_admin.id) - end - - it "treats the e-mail for login case insensitively" do - request.env["omniauth.auth"] = @case_insensitive_omniauth - - get(provider) - session["warden.user.admin.key"].should be_kind_of(Array) - session["warden.user.admin.key"][0].should be_kind_of(Array) - admin_id = session["warden.user.admin.key"][0][0] - admin_id.should eql(@case_insensitive_admin.id) - end - - it "denies non-existent admins" do - OmniAuth.config.mock_auth[provider] = @nonexistent_omniauth - request.env["omniauth.auth"] = OmniAuth.config.mock_auth[provider] - - get(provider) - session["warden.user.admin.key"].should eql(nil) - end - end - - shared_examples "omniauth verified e-mail login" do |provider| - it "denies unverified e-mail addresses" do - OmniAuth.config.mock_auth[provider] = @unverified_omniauth - request.env["omniauth.auth"] = OmniAuth.config.mock_auth[provider] - - get(provider) - session["warden.user.admin.key"].should eql(nil) - end - end - - describe "cas" do - before(:all) do - @valid_omniauth = OmniAuth::AuthHash.new({ - :provider => "cas", - :uid => @valid_admin.username, - }) - - @case_insensitive_omniauth = @valid_omniauth.deep_dup - @case_insensitive_omniauth[:uid] = "Hello@ExamplE.Com" - - @nonexistent_omniauth = @valid_omniauth.deep_dup - @nonexistent_omniauth[:uid] = "bad@example.com" - end - - it_behaves_like "omniauth login", :cas - end - - describe "facebook" do - before(:all) do - @valid_omniauth = OmniAuth::AuthHash.new({ - :provider => "facebook", - :uid => "12345", - :info => { - :email => @valid_admin.username, - :verified => true, - }, - }) - - @case_insensitive_omniauth = @valid_omniauth.deep_dup - @case_insensitive_omniauth[:info][:email] = "Hello@ExamplE.Com" - - @nonexistent_omniauth = @valid_omniauth.deep_dup - @nonexistent_omniauth[:info][:email] = "bad@example.com" - - @unverified_omniauth = @valid_omniauth.deep_dup - @unverified_omniauth[:info][:verified] = false - end - - it_behaves_like "omniauth login", :facebook - it_behaves_like "omniauth verified e-mail login", :facebook - end - - describe "github" do - before(:all) do - @valid_omniauth = OmniAuth::AuthHash.new({ - :provider => "github", - :uid => "12345", - :info => { - :email => @valid_admin.username, - :email_verified => true, - }, - }) - - @case_insensitive_omniauth = @valid_omniauth.deep_dup - @case_insensitive_omniauth[:info][:email] = "Hello@ExamplE.Com" - - @nonexistent_omniauth = @valid_omniauth.deep_dup - @nonexistent_omniauth[:info][:email] = "bad@example.com" - - @unverified_omniauth = @valid_omniauth.deep_dup - @unverified_omniauth[:info][:email_verified] = false - end - - it_behaves_like "omniauth login", :github - it_behaves_like "omniauth verified e-mail login", :github - end - - describe "google_oauth2" do - before(:all) do - @valid_omniauth = OmniAuth::AuthHash.new({ - :provider => "google_oauth2", - :uid => "12345", - :info => { - :email => @valid_admin.username, - }, - :extra => { - :raw_info => { - :email_verified => true, - }, - }, - }) - - @case_insensitive_omniauth = @valid_omniauth.deep_dup - @case_insensitive_omniauth[:info][:email] = "Hello@ExamplE.Com" - - @nonexistent_omniauth = @valid_omniauth.deep_dup - @nonexistent_omniauth[:info][:email] = "bad@example.com" - - @unverified_omniauth = @valid_omniauth.deep_dup - @unverified_omniauth[:extra][:raw_info][:email_verified] = false - end - - it_behaves_like "omniauth login", :google_oauth2 - it_behaves_like "omniauth verified e-mail login", :google_oauth2 - end - - describe "myusa" do - before(:all) do - @valid_omniauth = OmniAuth::AuthHash.new({ - :provider => "myusa", - :uid => "12345", - :info => { - :email => @valid_admin.username, - }, - }) - - @case_insensitive_omniauth = @valid_omniauth.deep_dup - @case_insensitive_omniauth[:info][:email] = "Hello@ExamplE.Com" - - @nonexistent_omniauth = @valid_omniauth.deep_dup - @nonexistent_omniauth[:info][:email] = "bad@example.com" - end - - it_behaves_like "omniauth login", :myusa - end - - describe "persona" do - before(:all) do - @valid_omniauth = OmniAuth::AuthHash.new({ - :provider => "persona", - :uid => "12345", - :info => { - :email => @valid_admin.username, - }, - }) - - @case_insensitive_omniauth = @valid_omniauth.deep_dup - @case_insensitive_omniauth[:info][:email] = "Hello@ExamplE.Com" - - @nonexistent_omniauth = @valid_omniauth.deep_dup - @nonexistent_omniauth[:info][:email] = "bad@example.com" - end - - it_behaves_like "omniauth login", :persona - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/admin/base_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/admin/base_controller_spec.rb deleted file mode 100644 index 5a62079a5..000000000 --- a/src/api-umbrella/web-app/spec/controllers/admin/base_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Admin::BaseController do - -end diff --git a/src/api-umbrella/web-app/spec/controllers/admin/sessions_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/admin/sessions_controller_spec.rb deleted file mode 100644 index c9d624eae..000000000 --- a/src/api-umbrella/web-app/spec/controllers/admin/sessions_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Admin::SessionsController do - -end diff --git a/src/api-umbrella/web-app/spec/controllers/admin/stats_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/admin/stats_controller_spec.rb deleted file mode 100644 index 843280f6e..000000000 --- a/src/api-umbrella/web-app/spec/controllers/admin/stats_controller_spec.rb +++ /dev/null @@ -1,328 +0,0 @@ -require 'spec_helper' -require 'test_helper/elasticsearch_helper' - -describe Admin::StatsController do - login_admin - - before(:each) do - ElasticsearchHelper.clean_es_indices(["2014-11", "2015-01", "2015-03"]) - end - - describe "GET search" do - it "bins the results by day with proper time zone" do - Time.use_zone("America/Denver") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-12T23:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-13T00:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-18T23:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-19T00:00:00")) - end - LogItem.gateway.refresh_index! - - get :search, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-01-13", - :end_at => "2015-01-18", - :interval => "day", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["stats"]["total_hits"].should eql(2) - data["hits_over_time"][0]["c"][0]["f"].should eql("Tue, Jan 13, 2015") - data["hits_over_time"][0]["c"][0]["v"].should eql(1421132400000) - data["hits_over_time"][0]["c"][1]["f"].should eql("1") - data["hits_over_time"][0]["c"][1]["v"].should eql(1) - data["hits_over_time"][5]["c"][0]["f"].should eql("Sun, Jan 18, 2015") - data["hits_over_time"][5]["c"][0]["v"].should eql(1421564400000) - data["hits_over_time"][5]["c"][1]["f"].should eql("1") - data["hits_over_time"][5]["c"][1]["v"].should eql(1) - end - - it "bins the daily results properly when daylight savings time begins" do - LogItem.index_name = "api-umbrella-logs-write-2015-03" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T00:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T09:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-09T10:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :search, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-03-07", - :end_at => "2015-03-09", - :interval => "day", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["stats"]["total_hits"].should eql(4) - data["hits_over_time"][0]["c"][0]["f"].should eql("Sat, Mar 7, 2015") - data["hits_over_time"][0]["c"][0]["v"].should eql(1425711600000) - data["hits_over_time"][0]["c"][1]["f"].should eql("1") - data["hits_over_time"][0]["c"][1]["v"].should eql(1) - data["hits_over_time"][1]["c"][0]["f"].should eql("Sun, Mar 8, 2015") - data["hits_over_time"][1]["c"][0]["v"].should eql(1425798000000) - data["hits_over_time"][1]["c"][1]["f"].should eql("2") - data["hits_over_time"][1]["c"][1]["v"].should eql(2) - data["hits_over_time"][2]["c"][0]["f"].should eql("Mon, Mar 9, 2015") - data["hits_over_time"][2]["c"][0]["v"].should eql(1425880800000) - data["hits_over_time"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"][2]["c"][1]["v"].should eql(1) - end - - it "bins the hourly results properly when daylight savings time begins" do - LogItem.index_name = "api-umbrella-logs-write-2015-03" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T09:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :search, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-03-08", - :end_at => "2015-03-08", - :interval => "hour", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["stats"]["total_hits"].should eql(2) - data["hits_over_time"][0]["c"][0]["f"].should eql("Sun, Mar 8, 2015 12:00am MST") - data["hits_over_time"][0]["c"][0]["v"].should eql(1425798000000) - data["hits_over_time"][0]["c"][1]["f"].should eql("0") - data["hits_over_time"][0]["c"][1]["v"].should eql(0) - data["hits_over_time"][1]["c"][0]["f"].should eql("Sun, Mar 8, 2015 1:00am MST") - data["hits_over_time"][1]["c"][0]["v"].should eql(1425801600000) - data["hits_over_time"][1]["c"][1]["f"].should eql("1") - data["hits_over_time"][1]["c"][1]["v"].should eql(1) - data["hits_over_time"][2]["c"][0]["f"].should eql("Sun, Mar 8, 2015 3:00am MDT") - data["hits_over_time"][2]["c"][0]["v"].should eql(1425805200000) - data["hits_over_time"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"][2]["c"][1]["v"].should eql(1) - data["hits_over_time"][3]["c"][0]["f"].should eql("Sun, Mar 8, 2015 4:00am MDT") - data["hits_over_time"][3]["c"][0]["v"].should eql(1425808800000) - data["hits_over_time"][3]["c"][1]["f"].should eql("0") - data["hits_over_time"][3]["c"][1]["v"].should eql(0) - end - - it "bins the daily results properly when daylight savings time ends" do - LogItem.index_name = "api-umbrella-logs-write-2014-11" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T00:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T09:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-03T10:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :search, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2014-11-01", - :end_at => "2014-11-03", - :interval => "day", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["stats"]["total_hits"].should eql(4) - data["hits_over_time"][0]["c"][0]["f"].should eql("Sat, Nov 1, 2014") - data["hits_over_time"][0]["c"][0]["v"].should eql(1414821600000) - data["hits_over_time"][0]["c"][1]["f"].should eql("1") - data["hits_over_time"][0]["c"][1]["v"].should eql(1) - data["hits_over_time"][1]["c"][0]["f"].should eql("Sun, Nov 2, 2014") - data["hits_over_time"][1]["c"][0]["v"].should eql(1414908000000) - data["hits_over_time"][1]["c"][1]["f"].should eql("2") - data["hits_over_time"][1]["c"][1]["v"].should eql(2) - data["hits_over_time"][2]["c"][0]["f"].should eql("Mon, Nov 3, 2014") - data["hits_over_time"][2]["c"][0]["v"].should eql(1414998000000) - data["hits_over_time"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"][2]["c"][1]["v"].should eql(1) - end - - it "bins the hourly results properly when daylight savings time ends" do - LogItem.index_name = "api-umbrella-logs-write-2014-11" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T09:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :search, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2014-11-02", - :end_at => "2014-11-02", - :interval => "hour", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["stats"]["total_hits"].should eql(2) - data["hits_over_time"][1]["c"][0]["f"].should eql("Sun, Nov 2, 2014 1:00am MDT") - data["hits_over_time"][1]["c"][0]["v"].should eql(1414911600000) - data["hits_over_time"][1]["c"][1]["f"].should eql("0") - data["hits_over_time"][1]["c"][1]["v"].should eql(0) - data["hits_over_time"][2]["c"][0]["f"].should eql("Sun, Nov 2, 2014 1:00am MST") - data["hits_over_time"][2]["c"][0]["v"].should eql(1414915200000) - data["hits_over_time"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"][2]["c"][1]["v"].should eql(1) - data["hits_over_time"][3]["c"][0]["f"].should eql("Sun, Nov 2, 2014 2:00am MST") - data["hits_over_time"][3]["c"][0]["v"].should eql(1414918800000) - data["hits_over_time"][3]["c"][1]["f"].should eql("1") - data["hits_over_time"][3]["c"][1]["v"].should eql(1) - data["hits_over_time"][4]["c"][0]["f"].should eql("Sun, Nov 2, 2014 3:00am MST") - data["hits_over_time"][4]["c"][0]["v"].should eql(1414922400000) - data["hits_over_time"][4]["c"][1]["f"].should eql("0") - data["hits_over_time"][4]["c"][1]["v"].should eql(0) - end - end - - describe "GET logs" do - it "strips the api_key from the request_url on the JSON response" do - FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :request_url => "http://127.0.0.1/with_api_key/?foo=bar&api_key=my_secret_key", :request_query => { "foo" => "bar", "api_key" => "my_secret_key" }) - LogItem.gateway.refresh_index! - - get :logs, { - "format" => "json", - "tz" => "America/Denver", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - "start" => "0", - "length" => "10", - } - - response.status.should eql(200) - body = response.body - data = MultiJson.load(body) - data["recordsTotal"].should eql(1) - data["data"][0]["request_url"].should eql("/with_api_key/?foo=bar") - data["data"][0]["request_query"].should eql({ "foo" => "bar" }) - body.should_not include("my_secret_key") - end - - it "strips the api_key from the request_url on the CSV response" do - FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :request_url => "http://127.0.0.1/with_api_key/?api_key=my_secret_key&foo=bar", :request_query => { "foo" => "bar", "api_key" => "my_secret_key" }) - LogItem.gateway.refresh_index! - - get :logs, { - "format" => "csv", - "tz" => "America/Denver", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - "start" => "0", - "length" => "10", - } - - response.status.should eql(200) - body = response.body - body.should include(",http://127.0.0.1/with_api_key/?foo=bar,") - body.should_not include("my_secret_key") - end - - it "downloads a CSV that requires an elasticsearch scan and scroll query" do - FactoryGirl.create_list(:log_item, 1005, :request_at => Time.parse("2015-01-16T06:06:28.816Z")) - LogItem.gateway.refresh_index! - - get :logs, { - "format" => "csv", - "tz" => "America/Denver", - "search" => "", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - } - - response.status.should eql(200) - response.headers["Content-Type"].should eql("text/csv") - response.headers["Content-Disposition"].should include("attachment; filename=\"api_logs (#{Time.now.strftime("%b %-e %Y")}).csv\"") - - lines = response.body.split("\n") - lines[0].should eql("Time,Method,Host,URL,User,IP Address,Country,State,City,Status,Reason Denied,Response Time,Content Type,Accept Encoding,User Agent") - lines.length.should eql(1006) - end - - describe "query builder" do - it "searches fields case-insensitively by default" do - FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :request_user_agent => "MOZILLAAA") - LogItem.gateway.refresh_index! - - get :logs, { - "format" => "json", - "tz" => "America/Denver", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - "start" => "0", - "length" => "10", - "query" => '{"condition":"AND","rules":[{"id":"request_user_agent","field":"request_user_agent","type":"string","input":"text","operator":"begins_with","value":"Mozilla"}]}' - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(1) - data["data"][0]["request_user_agent"].should eql("MOZILLAAA") - end - - it "matches the api key case-sensitively" do - FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :api_key => "AbCDeF", :request_user_agent => "api key match test") - LogItem.gateway.refresh_index! - - get :logs, { - "format" => "json", - "tz" => "America/Denver", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - "start" => "0", - "length" => "10", - "query" => '{"condition":"AND","rules":[{"id":"api_key","field":"api_key","type":"string","input":"text","operator":"begins_with","value":"AbCDeF"}]}' - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(1) - data["data"][0]["request_user_agent"].should eql("api key match test") - end - - it "operates properly with null operators and a null value" do - FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :request_user_agent => "gatekeeper denied code null test") - FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :gatekeeper_denied_code => "api_key_missing", :request_user_agent => "gatekeeper denied code not null test") - LogItem.gateway.refresh_index! - - get :logs, { - "format" => "json", - "tz" => "America/Denver", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - "start" => "0", - "length" => "10", - "query" => '{"condition":"AND","rules":[{"id":"gatekeeper_denied_code","field":"gatekeeper_denied_code","type":"string","input":"select","operator":"is_not_null","value":null}]}' - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(1) - data["data"][0]["request_user_agent"].should eql("gatekeeper denied code not null test") - end - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/api_users_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/api_users_controller_spec.rb deleted file mode 100644 index 0207283e8..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/api_users_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Api::ApiUsersController do - -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/health_checks_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/health_checks_controller_spec.rb deleted file mode 100644 index 07c4ade8a..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/health_checks_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Api::HealthChecksController do - -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/admin_groups_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/admin_groups_controller_spec.rb deleted file mode 100644 index 3a1810bfb..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/admin_groups_controller_spec.rb +++ /dev/null @@ -1,372 +0,0 @@ -require 'spec_helper' - -describe Api::V1::AdminGroupsController do - before(:each) do - DatabaseCleaner.clean - end - - describe "GET index" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - describe "admin_usernames" do - it "returns an array of admin usernames belonging to the group" do - group = FactoryGirl.create(:admin_group) - admin_in_group = FactoryGirl.create(:limited_admin, :groups => [ - group, - ]) - - admin_token_auth(@admin) - get :index, :format => "json" - - data = MultiJson.load(response.body) - data["data"].length.should eql(1) - data["data"][0]["admin_usernames"].should eql([admin_in_group.username]) - end - - it "sorts usernames in alphabetical order" do - group = FactoryGirl.create(:admin_group) - admin_in_group1 = FactoryGirl.create(:limited_admin, :username => "b", :groups => [ - group, - ]) - admin_in_group2 = FactoryGirl.create(:limited_admin, :username => "a", :groups => [ - group, - ]) - - admin_token_auth(@admin) - get :index, :format => "json" - - data = MultiJson.load(response.body) - data["data"].length.should eql(1) - data["data"][0]["admin_usernames"].should eql([admin_in_group2.username, admin_in_group1.username]) - end - - it "returns an empty array when no admins belong to a group" do - FactoryGirl.create(:admin_group) - - admin_token_auth(@admin) - get :index, :format => "json" - - data = MultiJson.load(response.body) - data["data"].length.should eql(1) - data["data"][0]["admin_usernames"].should eql([]) - end - end - end - - describe "GET show" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - describe "admins" do - it "returns metadata for the admins belonging to the group" do - group = FactoryGirl.create(:admin_group) - admin_in_group = FactoryGirl.create(:limited_admin, :last_sign_in_at => Time.now, :groups => [ - group, - ]) - - admin_token_auth(@admin) - get :show, :id => group.id, :format => "json" - - data = MultiJson.load(response.body) - data["admin_group"]["admins"].should eql([ - { - "id" => admin_in_group.id, - "username" => admin_in_group.username, - "last_sign_in_at" => admin_in_group.last_sign_in_at.iso8601, - }, - ]) - end - - it "sorts admins by username in alphabetical order" do - group = FactoryGirl.create(:admin_group) - admin_in_group1 = FactoryGirl.create(:limited_admin, :username => "b", :groups => [ - group, - ]) - admin_in_group2 = FactoryGirl.create(:limited_admin, :username => "a", :groups => [ - group, - ]) - - admin_token_auth(@admin) - get :show, :id => group.id, :format => "json" - - data = MultiJson.load(response.body) - data["admin_group"]["admins"].map { |admin| admin["id"] }.should eql([ - admin_in_group2.id, - admin_in_group1.id, - ]) - end - - it "returns an empty array when no admins belong to a group" do - group = FactoryGirl.create(:admin_group) - - admin_token_auth(@admin) - get :show, :id => group.id, :format => "json" - - data = MultiJson.load(response.body) - data["admin_group"]["admins"].should eql([]) - end - end - end - - describe "POST create" do - end - - describe "PUT update" do - end - - describe "DELETE destroy" do - end - - describe "admin permissions" do - shared_examples "admin permitted" do - describe "GET index" do - it "includes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should include(record.id) - end - end - - describe "GET show" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data.keys.should eql(["admin_group"]) - end - end - - describe "POST create" do - it "permits access" do - attributes = FactoryGirl.build(@factory).serializable_hash - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :admin_group => attributes - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["admin_group"]["name"].should_not eql(nil) - data["admin_group"]["name"].should eql(attributes["name"]) - end.to change { AdminGroup.count }.by(1) - end - end - - describe "PUT update" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["name"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :admin_group => attributes - - response.status.should eql(204) - record = AdminGroup.find(record.id) - record.name.should_not eql(nil) - record.name.should eql(attributes["name"]) - end - end - - describe "DELETE destroy" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - response.status.should eql(204) - end.to change { AdminGroup.count }.by(-1) - end - end - end - - shared_examples "admin forbidden" do - describe "GET index" do - it "excludes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should_not include(record.id) - end - end - - describe "GET show" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end - end - - describe "POST create" do - it "forbids access" do - attributes = FactoryGirl.build(@factory).serializable_hash - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :admin_group => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { AdminGroup.count }.by(0) - end - end - - describe "PUT update" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["name"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :admin_group => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = AdminGroup.find(record.id) - record.name.should_not eql(nil) - record.name.should_not eql(attributes["name"]) - end - end - - describe "DELETE destroy" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { AdminGroup.count }.by(0) - end - end - end - - describe "localhost/google* group (single scope)" do - before(:each) do - @google_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope)) - @factory = :google_admin_group - end - - it_behaves_like "admin permissions", :required_permissions => ["admin_manage"] - end - - describe "localhost/google* and localhost/yahoo* group (multi scope)" do - before(:each) do - @google_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope)) - @yahoo_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:yahoo_api_scope)) - @factory = :google_and_yahoo_multi_scope_admin_group - end - - describe "superuser" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* and localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - @yahoo_api_scope, - ]), - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @yahoo_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - end - - it "prevents limited admins from updating its own group to contain scopes outside the current permissions" do - record = FactoryGirl.create(:google_admin_group) - yahoo_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:yahoo_api_scope)) - - admin = FactoryGirl.create(:limited_admin, :groups => [record]) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["api_scope_ids"] << yahoo_api_scope.id - put :update, :format => "json", :id => record.id, :admin_group => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = AdminGroup.find(record.id) - record.api_scope_ids.length.should eql(1) - end - - it "prevents limited admins from updating forbidden groups to only contain scopes the admin does have permissions to" do - record = FactoryGirl.create(:yahoo_admin_group) - yahoo_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:yahoo_api_scope)) - google_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope)) - - admin = FactoryGirl.create(:google_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["api_scope_ids"] = google_api_scope.id - put :update, :format => "json", :id => record.id, :admin_group => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = AdminGroup.find(record.id) - record.api_scope_ids.should eql([yahoo_api_scope.id]) - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/admin_permissions_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/admin_permissions_spec.rb deleted file mode 100644 index 4c96187f4..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/admin_permissions_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "spec_helper" - -describe Api::V1::AdminPermissionsController do - before(:all) do - @admin = FactoryGirl.create(:admin) - end - - describe "GET index" do - it "returns the expected permissions in the display order" do - admin_token_auth(@admin) - get :index, :format => "json" - - data = MultiJson.load(response.body) - permission_names = data["admin_permissions"].map { |permission| permission["name"] } - permission_names.should eql([ - "Analytics", - "API Users - View", - "API Users - Manage", - "Admin Accounts - View & Manage", - "API Backend Configuration - View & Manage", - "API Backend Configuration - Publish", - ]) - - data["admin_permissions"].first["id"].should eql("analytics") - data["admin_permissions"].first["name"].should eql("Analytics") - data["admin_permissions"].first["display_order"].should eql(1) - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/admins_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/admins_controller_spec.rb deleted file mode 100644 index 50df191a1..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/admins_controller_spec.rb +++ /dev/null @@ -1,448 +0,0 @@ -require 'spec_helper' - -describe Api::V1::AdminsController do - before(:each) do - DatabaseCleaner.clean - end - - shared_examples "admin save permissions" do |method, action| - it "return validation error if account has no groups and isn't a superuser" do - attributes = FactoryGirl.build(:admin, { - :superuser => false, - :groups => [], - }).serializable_hash - - expect do - admin_token_auth(@google_admin) - send(method, action, params.merge(:admin => attributes)) - - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - data["errors"].first["field"].should eql("groups") - end.to_not change { Admin.count } - end - end - - describe "GET index" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - it "paginates results" do - FactoryGirl.create_list(:admin, 3) - - admin_token_auth(@admin) - get :index, :format => "json", :length => 2 - - admin_count = Admin.count - admin_count.should be > 2 - - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(admin_count) - data["recordsFiltered"].should eql(admin_count) - data["data"].length.should eql(2) - end - end - - describe "GET show" do - end - - describe "POST create" do - end - - describe "PUT update" do - end - - describe "DELETE destroy" do - end - - describe "validations" do - end - - describe "admin permissions" do - shared_examples "admin permitted" do - describe "GET index" do - it "includes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should include(record.id) - end - end - - describe "GET show" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data.keys.should eql(["admin"]) - end - end - - describe "POST create" do - it "permits access" do - attributes = FactoryGirl.build(@factory).serializable_hash - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :admin => attributes - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["admin"]["username"].should_not eql(nil) - data["admin"]["username"].should eql(attributes["username"]) - end.to change { Admin.count }.by(1) - end - end - - describe "PUT update" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["username"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["admin"]["username"].should_not eql(nil) - data["admin"]["username"].should eql(attributes["username"]) - - record = Admin.find(record.id) - record.username.should_not eql(nil) - record.username.should eql(attributes["username"]) - end - end - - describe "DELETE destroy" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - response.status.should eql(204) - end.to change { Admin.count }.by(-1) - end - end - end - - shared_examples "admin forbidden" do - describe "GET index" do - it "excludes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should_not include(record.id) - end - end - - describe "GET show" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end - end - - describe "POST create" do - it "forbids access" do - attributes = FactoryGirl.build(@factory).serializable_hash - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :admin => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { Admin.count }.by(0) - end - end - - describe "PUT update" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["username"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = Admin.find(record.id) - record.username.should_not eql(nil) - record.username.should_not eql(attributes["username"]) - end - end - - describe "DELETE destroy" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { Admin.count }.by(0) - end - end - end - - describe "localhost/google* admin (single group, single scope)" do - before(:each) do - @factory = :google_admin - end - - it_behaves_like "admin permissions", :required_permissions => ["admin_manage"] - end - - describe "localhost/google* and localhost/yahoo* admin (multi group, multi scope)" do - before(:each) do - @google_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope)) - @yahoo_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:yahoo_api_scope)) - @factory = :google_and_yahoo_multi_group_admin - end - - describe "superuser" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* and localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - @yahoo_api_scope, - ]), - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @yahoo_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - end - - describe "localhost/google* and localhost/yahoo* admin (single group, multi scope)" do - before(:each) do - @google_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope)) - @yahoo_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:yahoo_api_scope)) - @factory = :google_and_yahoo_single_group_admin - end - - describe "superuser" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* and localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - @yahoo_api_scope, - ]), - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @yahoo_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - end - - describe "superuser admin" do - before(:each) do - @factory = :admin - end - - describe "superuser" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - it_behaves_like "admin permitted" - end - - describe "localhost/* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:localhost_root_admin_group) - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:google_admin) - end - it_behaves_like "admin forbidden" - end - end - - it "prevents limited admins from adding the superuser attribute on an existing limited admin account" do - record = FactoryGirl.create(:limited_admin) - - admin = FactoryGirl.create(:limited_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["superuser"] = "1" - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(403) - record = Admin.find(record.id) - record.superuser.should eql(false) - end - - it "prevents limited admins from adding the superuser attribute on its own account" do - record = FactoryGirl.create(:limited_admin) - - admin_token_auth(record) - - attributes = record.serializable_hash - attributes["superuser"] = "1" - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(403) - record = Admin.find(record.id) - record.superuser.should eql(false) - end - - it "prevents limited admins from removing the superuser attribute on an existing superuser admin account" do - record = FactoryGirl.create(:limited_admin, :superuser => true) - - admin = FactoryGirl.create(:limited_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["superuser"] = "0" - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(403) - record = Admin.find(record.id) - record.superuser.should eql(true) - end - - it "prevents limited admins from updating forbidden admins to only contain groups the admin does have permissions to" do - google_admin_group = FactoryGirl.create(:google_admin_group) - yahoo_admin_group = FactoryGirl.create(:yahoo_admin_group) - record = FactoryGirl.create(:limited_admin, :groups => [yahoo_admin_group]) - - admin = FactoryGirl.create(:google_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["group_ids"] = google_admin_group.id - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = Admin.find(record.id) - record.group_ids.should eql([yahoo_admin_group.id]) - end - - it "permits superuser admins from adding the superuser attribute on an existing limited admin account" do - record = FactoryGirl.create(:limited_admin) - - admin = FactoryGirl.create(:admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["superuser"] = "1" - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(200) - record = Admin.find(record.id) - record.superuser.should eql(true) - end - - it "permits superuser admins from removing the superuser attribute on an existing superuser admin account" do - record = FactoryGirl.create(:limited_admin, :superuser => true) - - admin = FactoryGirl.create(:admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["superuser"] = "0" - put :update, :format => "json", :id => record.id, :admin => attributes - - response.status.should eql(200) - record = Admin.find(record.id) - record.superuser.should eql(false) - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/analytics_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/analytics_controller_spec.rb deleted file mode 100644 index 52f447e59..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/analytics_controller_spec.rb +++ /dev/null @@ -1,436 +0,0 @@ -require 'spec_helper' -require 'test_helper/elasticsearch_helper' - -describe Api::V1::AnalyticsController do - login_admin - - before(:each) do - ElasticsearchHelper.clean_es_indices(["2014-11", "2015-01", "2015-03"]) - end - - describe "GET drilldown" do - it "matches level 0 based on the prefix" do - FactoryGirl.create_list(:log_item, 2, :request_hierarchy => ["0/127.0.0.1/", "1/127.0.0.1/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create(:log_item, :request_hierarchy => ["0/example.com/", "1/example.com/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - LogItem.gateway.refresh_index! - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-01-13", - :end_at => "2015-01-18", - :interval => "day", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should eql(2) - data["results"][0].should eql({ - "depth" => 0, - "path" => "127.0.0.1/", - "terminal" => false, - "descendent_prefix" => "1/127.0.0.1/", - "hits" => 2, - }) - data["hits_over_time"]["cols"].should eql([ - { "id" => "date", "label" => "Date", "type" => "datetime" }, - { "id" => "0/127.0.0.1/", "label" => "127.0.0.1/", "type" => "number" }, - { "id" => "0/example.com/", "label" => "example.com/", "type" => "number" }, - ]) - data["hits_over_time"]["rows"].length.should eql(6) - data["hits_over_time"]["rows"][1].should eql({ "c" => [ - { "v" => 1421218800000, "f" => "Wed, Jan 14, 2015" }, - { "v" => 2, "f" => "2" }, - { "v" => 1, "f" => "1" }, - ] }) - end - - it "matches level 1 based on the prefix" do - FactoryGirl.create_list(:log_item, 2, :request_hierarchy => ["0/127.0.0.1/", "1/127.0.0.1/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create(:log_item, :request_hierarchy => ["0/example.com/", "1/example.com/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - LogItem.gateway.refresh_index! - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-01-13", - :end_at => "2015-01-18", - :interval => "day", - :prefix => "1/example.com/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should eql(1) - data["results"][0].should eql({ - "depth" => 1, - "path" => "example.com/hello", - "terminal" => true, - "descendent_prefix" => "2/example.com/hello", - "hits" => 1, - }) - data["hits_over_time"]["cols"].should eql([ - { "id" => "date", "label" => "Date", "type" => "datetime" }, - { "id" => "1/example.com/hello", "label" => "example.com/hello", "type" => "number" }, - ]) - data["hits_over_time"]["rows"].length.should eql(6) - data["hits_over_time"]["rows"][1].should eql({ "c" => [ - { "v" => 1421218800000, "f" => "Wed, Jan 14, 2015" }, - { "v" => 1, "f" => "1" }, - ] }) - end - - it "only matches results beginning with the prefix and not containing the prefix elsewhere in the strings" do - FactoryGirl.create_list(:log_item, 2, :request_hierarchy => ["0/127.0.0.1/", "1/127.0.0.1/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - # Ensure that the second element in the array also contains "0/" to - # ensure that the filtering and terms aggregations are both matching - # based on prefix only. - FactoryGirl.create(:log_item, :request_hierarchy => ["0/0/", "1/0/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create(:log_item, :request_hierarchy => ["foo/0/", "foo/0/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - LogItem.gateway.refresh_index! - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-01-13", - :end_at => "2015-01-18", - :interval => "day", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should eql(2) - data["results"][0].should eql({ - "depth" => 0, - "path" => "127.0.0.1/", - "terminal" => false, - "descendent_prefix" => "1/127.0.0.1/", - "hits" => 2, - }) - data["hits_over_time"]["cols"].should eql([ - { "id" => "date", "label" => "Date", "type" => "datetime" }, - { "id" => "0/127.0.0.1/", "label" => "127.0.0.1/", "type" => "number" }, - { "id" => "0/0/", "label" => "0/", "type" => "number" }, - ]) - data["hits_over_time"]["rows"].length.should eql(6) - data["hits_over_time"]["rows"][1].should eql({ "c" => [ - { "v" => 1421218800000, "f" => "Wed, Jan 14, 2015" }, - { "v" => 2, "f" => "2" }, - { "v" => 1, "f" => "1" }, - ] }) - end - - it "performs an exact match on the prefix value, escaping any regex patterns" do - FactoryGirl.create_list(:log_item, 2, :request_hierarchy => ["0/127.0.0.1/", "1/127.0.0.1/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create(:log_item, :request_hierarchy => ["0/example.com/", "1/example.com/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - # Add other items in the request_hierarchy array that would match "0/." - # (even though this isn't really a valid hierarchy definition). This - # ensures that we also test whether the terms aggregations are being - # escaped (and not just the overall filter). - FactoryGirl.create(:log_item, :request_hierarchy => ["0/.com/", "0/xcom", "0/ycom", "1/.com/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - LogItem.gateway.refresh_index! - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-01-13", - :end_at => "2015-01-18", - :interval => "day", - :prefix => "0/.", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should eql(1) - data["results"][0].should eql({ - "depth" => 0, - "path" => ".com/", - "terminal" => false, - "descendent_prefix" => "1/.com/", - "hits" => 1, - }) - data["hits_over_time"]["cols"].should eql([ - { "id" => "date", "label" => "Date", "type" => "datetime" }, - { "id" => "0/.com/", "label" => ".com/", "type" => "number" }, - ]) - data["hits_over_time"]["rows"].length.should eql(6) - data["hits_over_time"]["rows"][1].should eql({ "c" => [ - { "v" => 1421218800000, "f" => "Wed, Jan 14, 2015" }, - { "v" => 1, "f" => "1" }, - ] }) - end - - it "returns all results for the list, but only the top 10 and an 'other' category for the chart" do - FactoryGirl.create_list(:log_item, 2, :request_hierarchy => ["0/127.0.0.1/", "1/127.0.0.1/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 2, :request_hierarchy => ["0/127.0.0.2/", "1/127.0.0.2/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 3, :request_hierarchy => ["0/127.0.0.3/", "1/127.0.0.3/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 10, :request_hierarchy => ["0/127.0.0.4/", "1/127.0.0.4/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 11, :request_hierarchy => ["0/127.0.0.5/", "1/127.0.0.5/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 12, :request_hierarchy => ["0/127.0.0.6/", "1/127.0.0.6/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 13, :request_hierarchy => ["0/127.0.0.7/", "1/127.0.0.7/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 14, :request_hierarchy => ["0/127.0.0.8/", "1/127.0.0.8/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 15, :request_hierarchy => ["0/127.0.0.9/", "1/127.0.0.9/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 16, :request_hierarchy => ["0/127.0.0.10/", "1/127.0.0.10/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 17, :request_hierarchy => ["0/127.0.0.11/", "1/127.0.0.11/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 18, :request_hierarchy => ["0/127.0.0.12/", "1/127.0.0.12/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - FactoryGirl.create_list(:log_item, 1, :request_hierarchy => ["0/127.0.0.13/", "1/127.0.0.13/hello"], :request_at => Time.parse("2015-01-15T00:00:00Z")) - LogItem.gateway.refresh_index! - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-01-13", - :end_at => "2015-01-18", - :interval => "day", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should eql(13) - data["results"][0].should eql({ - "depth" => 0, - "path" => "127.0.0.12/", - "terminal" => false, - "descendent_prefix" => "1/127.0.0.12/", - "hits" => 18, - }) - data["results"][12].should eql({ - "depth" => 0, - "path" => "127.0.0.13/", - "terminal" => false, - "descendent_prefix" => "1/127.0.0.13/", - "hits" => 1, - }) - data["hits_over_time"]["cols"].should eql([ - { "id" => "date", "label" => "Date", "type" => "datetime" }, - { "id" => "0/127.0.0.12/", "label" => "127.0.0.12/", "type" => "number" }, - { "id" => "0/127.0.0.11/", "label" => "127.0.0.11/", "type" => "number" }, - { "id" => "0/127.0.0.10/", "label" => "127.0.0.10/", "type" => "number" }, - { "id" => "0/127.0.0.9/", "label" => "127.0.0.9/", "type" => "number" }, - { "id" => "0/127.0.0.8/", "label" => "127.0.0.8/", "type" => "number" }, - { "id" => "0/127.0.0.7/", "label" => "127.0.0.7/", "type" => "number" }, - { "id" => "0/127.0.0.6/", "label" => "127.0.0.6/", "type" => "number" }, - { "id" => "0/127.0.0.5/", "label" => "127.0.0.5/", "type" => "number" }, - { "id" => "0/127.0.0.4/", "label" => "127.0.0.4/", "type" => "number" }, - { "id" => "0/127.0.0.3/", "label" => "127.0.0.3/", "type" => "number" }, - { "id" => "other", "label" => "Other", "type" => "number" }, - ]) - data["hits_over_time"]["rows"].length.should eql(6) - data["hits_over_time"]["rows"][1].should eql({ "c" => [ - { "v" => 1421218800000, "f" => "Wed, Jan 14, 2015" }, - { "v" => 18, "f" => "18" }, - { "v" => 17, "f" => "17" }, - { "v" => 16, "f" => "16" }, - { "v" => 15, "f" => "15" }, - { "v" => 14, "f" => "14" }, - { "v" => 13, "f" => "13" }, - { "v" => 12, "f" => "12" }, - { "v" => 11, "f" => "11" }, - { "v" => 10, "f" => "10" }, - { "v" => 3, "f" => "3" }, - { "v" => 5, "f" => "5" }, - ] }) - end - - it "bins the results by day with proper time zone" do - Time.use_zone("America/Denver") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-12T23:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-13T00:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-18T23:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-01-19T00:00:00")) - end - LogItem.gateway.refresh_index! - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-01-13", - :end_at => "2015-01-18", - :interval => "day", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should be > 0 - data["results"][0]["hits"].should eql(2) - data["hits_over_time"]["rows"][0]["c"][0]["f"].should eql("Tue, Jan 13, 2015") - data["hits_over_time"]["rows"][0]["c"][0]["v"].should eql(1421132400000) - data["hits_over_time"]["rows"][0]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][0]["c"][1]["v"].should eql(1) - data["hits_over_time"]["rows"][5]["c"][0]["f"].should eql("Sun, Jan 18, 2015") - data["hits_over_time"]["rows"][5]["c"][0]["v"].should eql(1421564400000) - data["hits_over_time"]["rows"][5]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][5]["c"][1]["v"].should eql(1) - end - - it "bins the daily results properly when daylight savings time begins" do - LogItem.index_name = "api-umbrella-logs-write-2015-03" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T00:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T09:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-09T10:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-03-07", - :end_at => "2015-03-09", - :interval => "day", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should be > 0 - data["results"][0]["hits"].should eql(4) - data["hits_over_time"]["rows"][0]["c"][0]["f"].should eql("Sat, Mar 7, 2015") - data["hits_over_time"]["rows"][0]["c"][0]["v"].should eql(1425711600000) - data["hits_over_time"]["rows"][0]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][0]["c"][1]["v"].should eql(1) - data["hits_over_time"]["rows"][1]["c"][0]["f"].should eql("Sun, Mar 8, 2015") - data["hits_over_time"]["rows"][1]["c"][0]["v"].should eql(1425798000000) - data["hits_over_time"]["rows"][1]["c"][1]["f"].should eql("2") - data["hits_over_time"]["rows"][1]["c"][1]["v"].should eql(2) - data["hits_over_time"]["rows"][2]["c"][0]["f"].should eql("Mon, Mar 9, 2015") - data["hits_over_time"]["rows"][2]["c"][0]["v"].should eql(1425880800000) - data["hits_over_time"]["rows"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][2]["c"][1]["v"].should eql(1) - end - - it "bins the hourly results properly when daylight savings time begins" do - LogItem.index_name = "api-umbrella-logs-write-2015-03" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2015-03-08T09:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2015-03-08", - :end_at => "2015-03-08", - :interval => "hour", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should be > 0 - data["results"][0]["hits"].should eql(2) - data["hits_over_time"]["rows"][0]["c"][0]["f"].should eql("Sun, Mar 8, 2015 12:00am MST") - data["hits_over_time"]["rows"][0]["c"][0]["v"].should eql(1425798000000) - data["hits_over_time"]["rows"][0]["c"][1]["f"].should eql("0") - data["hits_over_time"]["rows"][0]["c"][1]["v"].should eql(0) - data["hits_over_time"]["rows"][1]["c"][0]["f"].should eql("Sun, Mar 8, 2015 1:00am MST") - data["hits_over_time"]["rows"][1]["c"][0]["v"].should eql(1425801600000) - data["hits_over_time"]["rows"][1]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][1]["c"][1]["v"].should eql(1) - data["hits_over_time"]["rows"][2]["c"][0]["f"].should eql("Sun, Mar 8, 2015 3:00am MDT") - data["hits_over_time"]["rows"][2]["c"][0]["v"].should eql(1425805200000) - data["hits_over_time"]["rows"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][2]["c"][1]["v"].should eql(1) - data["hits_over_time"]["rows"][3]["c"][0]["f"].should eql("Sun, Mar 8, 2015 4:00am MDT") - data["hits_over_time"]["rows"][3]["c"][0]["v"].should eql(1425808800000) - data["hits_over_time"]["rows"][3]["c"][1]["f"].should eql("0") - data["hits_over_time"]["rows"][3]["c"][1]["v"].should eql(0) - end - - it "bins the daily results properly when daylight savings time ends" do - LogItem.index_name = "api-umbrella-logs-write-2014-11" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T00:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T09:00:00")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-03T10:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2014-11-01", - :end_at => "2014-11-03", - :interval => "day", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should be > 0 - data["results"][0]["hits"].should eql(4) - data["hits_over_time"]["rows"][0]["c"][0]["f"].should eql("Sat, Nov 1, 2014") - data["hits_over_time"]["rows"][0]["c"][0]["v"].should eql(1414821600000) - data["hits_over_time"]["rows"][0]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][0]["c"][1]["v"].should eql(1) - data["hits_over_time"]["rows"][1]["c"][0]["f"].should eql("Sun, Nov 2, 2014") - data["hits_over_time"]["rows"][1]["c"][0]["v"].should eql(1414908000000) - data["hits_over_time"]["rows"][1]["c"][1]["f"].should eql("2") - data["hits_over_time"]["rows"][1]["c"][1]["v"].should eql(2) - data["hits_over_time"]["rows"][2]["c"][0]["f"].should eql("Mon, Nov 3, 2014") - data["hits_over_time"]["rows"][2]["c"][0]["v"].should eql(1414998000000) - data["hits_over_time"]["rows"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][2]["c"][1]["v"].should eql(1) - end - - it "bins the hourly results properly when daylight savings time ends" do - LogItem.index_name = "api-umbrella-logs-write-2014-11" - Time.use_zone("UTC") do - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T08:59:59")) - FactoryGirl.create(:log_item, :request_at => Time.zone.parse("2014-11-02T09:00:00")) - end - LogItem.gateway.refresh_index! - LogItem.index_name = "api-umbrella-logs-write-2015-01" - - get :drilldown, { - :format => "json", - :tz => "America/Denver", - :search => "", - :start_at => "2014-11-02", - :end_at => "2014-11-02", - :interval => "hour", - :prefix => "0/", - } - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["results"].length.should be > 0 - data["results"][0]["hits"].should eql(2) - data["hits_over_time"]["rows"][1]["c"][0]["f"].should eql("Sun, Nov 2, 2014 1:00am MDT") - data["hits_over_time"]["rows"][1]["c"][0]["v"].should eql(1414911600000) - data["hits_over_time"]["rows"][1]["c"][1]["f"].should eql("0") - data["hits_over_time"]["rows"][1]["c"][1]["v"].should eql(0) - data["hits_over_time"]["rows"][2]["c"][0]["f"].should eql("Sun, Nov 2, 2014 1:00am MST") - data["hits_over_time"]["rows"][2]["c"][0]["v"].should eql(1414915200000) - data["hits_over_time"]["rows"][2]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][2]["c"][1]["v"].should eql(1) - data["hits_over_time"]["rows"][3]["c"][0]["f"].should eql("Sun, Nov 2, 2014 2:00am MST") - data["hits_over_time"]["rows"][3]["c"][0]["v"].should eql(1414918800000) - data["hits_over_time"]["rows"][3]["c"][1]["f"].should eql("1") - data["hits_over_time"]["rows"][3]["c"][1]["v"].should eql(1) - data["hits_over_time"]["rows"][4]["c"][0]["f"].should eql("Sun, Nov 2, 2014 3:00am MST") - data["hits_over_time"]["rows"][4]["c"][0]["v"].should eql(1414922400000) - data["hits_over_time"]["rows"][4]["c"][1]["f"].should eql("0") - data["hits_over_time"]["rows"][4]["c"][1]["v"].should eql(0) - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/api_scopes_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/api_scopes_controller_spec.rb deleted file mode 100644 index 88e3723a1..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/api_scopes_controller_spec.rb +++ /dev/null @@ -1,204 +0,0 @@ -require "spec_helper" - -describe Api::V1::ApiScopesController do - before(:each) do - DatabaseCleaner.clean - end - - describe "admin permissions" do - shared_examples "admin permitted" do - describe "GET index" do - it "includes the scope in the results" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should include(record.id) - end - end - - describe "GET show" do - it "permits access" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data.keys.should eql(["api_scope"]) - end - end - - describe "POST create" do - it "permits access" do - attributes = FactoryGirl.attributes_for(@factory).stringify_keys - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :api_scope => attributes - - # Validation errors may occur on some of the create tests, since we - # can't create duplicate records with the same hostname and prefix. - # This is expected to happen in some of the tests where we have to - # create a scope for the admin group we're authenticating as prior - # to this create attempt. - if(response.status == 422) - response.status.should eql(422) - data = MultiJson.load(response.body) - data.should eql("errors" => { "path_prefix" => ["is already taken"] }) - - # Add something extra to the path prefix, since create sub-scopes - # within an existing prefix should be permitted. - @path_prefix_increment ||= 0 - @path_prefix_increment += 1 - attributes["path_prefix"] += @path_prefix_increment.to_s - post :create, :format => "json", :api_scope => attributes - end - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api_scope"]["name"].should_not eql(nil) - data["api_scope"]["name"].should eql(attributes["name"]) - end.to change { ApiScope.count }.by(1) - end - end - - describe "PUT update" do - it "permits access" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["name"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :api_scope => attributes - - response.status.should eql(204) - record = ApiScope.find(record.id) - record.name.should_not eql(nil) - record.name.should eql(attributes["name"]) - end - end - - describe "DELETE destroy" do - it "permits access" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - response.status.should eql(204) - end.to change { ApiScope.count }.by(-1) - end - end - end - - shared_examples "admin forbidden" do - describe "GET index" do - it "excludes the group in the results" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should_not include(record.id) - end - end - - describe "GET show" do - it "forbids access" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end - end - - describe "POST create" do - it "forbids access" do - attributes = FactoryGirl.attributes_for(@factory).stringify_keys - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :api_scope => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { ApiScope.count }.by(0) - end - end - - describe "PUT update" do - it "forbids access" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["name"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :api_scope => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = ApiScope.find(record.id) - record.name.should_not eql(nil) - record.name.should_not eql(attributes["name"]) - end - end - - describe "DELETE destroy" do - it "forbids access" do - record = ApiScope.find_or_create_by_instance!(FactoryGirl.build(@factory)) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { ApiScope.count }.by(0) - end - end - end - - describe "localhost/google* scope" do - before(:each) do - @factory = :google_api_scope - end - - it_behaves_like "admin permissions", :required_permissions => ["admin_manage"] - end - - it "prevents limited admins from updating forbidden scopes to only use scopes the admin does have permissions to" do - record = FactoryGirl.create(:yahoo_api_scope) - - admin = FactoryGirl.create(:google_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["path_prefix"] = "/google/#{rand(999_999)}" - put :update, :format => "json", :id => record.id, :api_scope => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = ApiScope.find(record.id) - record.path_prefix.should eql("/yahoo") - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/apis_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/apis_controller_spec.rb deleted file mode 100644 index 8aee97c8a..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/apis_controller_spec.rb +++ /dev/null @@ -1,1546 +0,0 @@ -require 'spec_helper' - -describe Api::V1::ApisController do - before(:each) do - DatabaseCleaner.clean - end - - shared_examples "validates nested attributes presence - create" do |field| - it "returns a validation error if #{field} is set to nil" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - field => nil, - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - data["errors"].should eql({ - "base" => ["must have at least one #{field}"], - }) - end.to_not change { Api.count } - end - - it "returns a validation error if #{field} is set to an empty array" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - field => [], - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - data["errors"].should eql({ - "base" => ["must have at least one #{field}"], - }) - end.to_not change { Api.count } - end - - it "accepts the input if at least one url prefix exists" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - field => [FactoryGirl.attributes_for(:"api_#{field.to_s.singularize}")], - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["name"].should eql(attributes[:name]) - end.to change { Api.count }.by(1) - end - end - - shared_examples "validates nested attributes presence - update" do |field| - before(:each) do - @api = FactoryGirl.create(:api) - end - - it "returns a validation error if #{field} is set to nil" do - admin_token_auth(@admin) - attributes = @api.serializable_hash - attributes[field.to_s] = nil - - put :update, :format => "json", :id => @api.id, :api => attributes - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - data["errors"].should eql({ - "base" => ["must have at least one #{field}"], - }) - end - - it "returns a validation error if #{field} is set to an empty array" do - admin_token_auth(@admin) - attributes = @api.serializable_hash - attributes[field.to_s] = [] - - put :update, :format => "json", :id => @api.id, :api => attributes - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - data["errors"].should eql({ - "base" => ["must have at least one #{field}"], - }) - end - - it "accepts the input if at least one #{field} exists" do - admin_token_auth(@admin) - attributes = @api.serializable_hash - attributes[field.to_s] = [FactoryGirl.attributes_for(:"api_#{field.to_s.singularize}")] - - put :update, :format => "json", :id => @api.id, :api => attributes - response.status.should eql(204) - @api = Api.find(@api.id) - @api[field].length.should eql(1) - end - end - - shared_examples "api settings header fields - show" do |field| - it "returns no headers as an empty string" do - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - }), - }) - - admin_token_auth(@admin) - get :show, :format => "json", :id => api.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("") - end - - it "returns a single header as a string" do - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}" => [ - FactoryGirl.attributes_for(:api_header, { :key => "X-Add1", :value => "test1" }), - ], - }), - }) - - admin_token_auth(@admin) - get :show, :format => "json", :id => api.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("X-Add1: test1") - end - - it "returns multiple headers as a new-line separated string" do - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}" => [ - FactoryGirl.attributes_for(:api_header, { :key => "X-Add1", :value => "test1" }), - FactoryGirl.attributes_for(:api_header, { :key => "X-Add2", :value => "test2" }), - ], - }), - }) - - admin_token_auth(@admin) - get :show, :format => "json", :id => api.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("X-Add1: test1\nX-Add2: test2") - end - end - - shared_examples "api settings header fields - create" do |field| - it "accepts a nil value" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}_string" => nil, - }), - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("") - - api = Api.find(data["api"]["id"]) - api.settings.send(field).length.should eql(0) - end.to change { Api.count }.by(1) - end - - it "accepts an empty string" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}_string" => "", - }), - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("") - - api = Api.find(data["api"]["id"]) - api.settings.send(field).length.should eql(0) - end.to change { Api.count }.by(1) - end - - it "parses a single header from a string" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}_string" => "X-Add1: test1", - }), - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("X-Add1: test1") - - api = Api.find(data["api"]["id"]) - api.settings.send(field).length.should eql(1) - end.to change { Api.count }.by(1) - end - - it "parses a multiple headers from a string" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}_string" => "X-Add1: test1\nX-Add2: test2", - }), - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("X-Add1: test1\nX-Add2: test2") - - api = Api.find(data["api"]["id"]) - api.settings.send(field).length.should eql(2) - end.to change { Api.count }.by(1) - end - - it "strips extra whitespace from the string" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}_string" => "\n\n X-Add1:test1\n\n\nX-Add2: test2 \n\n", - }), - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("X-Add1: test1\nX-Add2: test2") - - api = Api.find(data["api"]["id"]) - api.settings.send(field).length.should eql(2) - end.to change { Api.count }.by(1) - end - - it "only parses to the first colon for the header name" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}_string" => "X-Add1: test1:test2", - }), - }) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["settings"]["#{field}_string"].should eql("X-Add1: test1:test2") - - api = Api.find(data["api"]["id"]) - api.settings.send(field).length.should eql(1) - end.to change { Api.count }.by(1) - end - end - - shared_examples "api settings header fields - update" do |field| - it "clears existing headers when passed nil" do - admin_token_auth(@admin) - - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}" => [ - FactoryGirl.attributes_for(:api_header, { :key => "X-Add1", :value => "test1" }), - ], - }), - }) - - api.settings.send(field).length.should eql(1) - - attributes = api.serializable_hash - attributes["settings"].delete(field.to_s) - attributes["settings"]["#{field}_string"] = nil - put :update, :format => "json", :id => api.id, :api => attributes - - response.status.should eql(204) - api = Api.find(api.id) - api.settings.send(field).length.should eql(0) - end - - it "clears existing headers when passed an empty string" do - admin_token_auth(@admin) - - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}" => [ - FactoryGirl.attributes_for(:api_header, { :key => "X-Add1", :value => "test1" }), - ], - }), - }) - - api.settings.send(field).length.should eql(1) - - attributes = api.serializable_hash - attributes["settings"].delete(field.to_s) - attributes["settings"]["#{field}_string"] = "" - put :update, :format => "json", :id => api.id, :api => attributes - - response.status.should eql(204) - api = Api.find(api.id) - api.settings.send(field).length.should eql(0) - end - - it "replaces existing headers when passed the headers string" do - admin_token_auth(@admin) - - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :"#{field}" => [ - FactoryGirl.attributes_for(:api_header, { :key => "X-Add1", :value => "test1" }), - ], - }), - }) - - api.settings.send(field).length.should eql(1) - api.settings.send(field).map { |h| h.key }.should eql(["X-Add1"]) - - attributes = api.serializable_hash - attributes["settings"].delete(field.to_s) - attributes["settings"]["#{field}_string"] = "X-New1: test1\nX-New2: test2" - put :update, :format => "json", :id => api.id, :api => attributes - - response.status.should eql(204) - api = Api.find(api.id) - api.settings.send(field).length.should eql(2) - api.settings.send(field).map { |h| h.key }.should eql(["X-New1", "X-New2"]) - end - end - - shared_examples "api settings error data yaml strings" do |method, action| - it "returns validation error for invalid yaml" do - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :error_data_yaml_strings => { - :api_key_invalid => "foo: &", - :api_key_missing => "foo: bar\nhello: `world", - }, - }), - }) - - admin_token_auth(@admin) - send(method, action, params.merge(:api => attributes)) - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - data["errors"].should eql({ - "settings.error_data_yaml_strings.api_key_invalid" => ["YAML parsing error: (): did not find expected alphabetic or numeric character while scanning an anchor at line 1 column 6"], - "settings.error_data_yaml_strings.api_key_missing" => ["YAML parsing error: (): found character that cannot start any token while scanning for the next token at line 2 column 8"], - }) - end - - it "returns validation error for yaml that isn't a hash" do - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :error_data_yaml_strings => { - :api_key_invalid => "foo", - }, - }), - }) - - admin_token_auth(@admin) - send(method, action, params.merge(:api => attributes)) - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - data["errors"].should eql({ - "settings.error_data.api_key_invalid" => ["unexpected type (must be a hash)"], - }) - end - - it "accepts a yaml of hash data" do - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :error_data_yaml_strings => { - :api_key_invalid => "status_code: 422\nfoo: bar", - }, - }), - }) - - admin_token_auth(@admin) - send(method, action, params.merge(:api => attributes)) - response.status.should eql(if(action == :create) then 201 else 204 end) - - api = Api.desc(:updated_at).first - api.settings.error_data.should eql({ - "api_key_invalid" => { - "status_code" => 422, - "foo" => "bar", - }, - }) - end - end - - describe "GET index" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - it "returns datatables output fields" do - admin_token_auth(@admin) - get :index, :format => "json" - - data = MultiJson.load(response.body) - data.keys.sort.should eql([ - "data", - "draw", - "recordsFiltered", - "recordsTotal", - ]) - end - - it "paginates results" do - FactoryGirl.create_list(:api, 3) - - admin_token_auth(@admin) - get :index, :format => "json", :length => "2" - - Api.count.should eql(3) - - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(3) - data["recordsFiltered"].should eql(3) - data["data"].length.should eql(2) - end - end - - describe "GET show" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - describe "request headers" do - it_behaves_like "api settings header fields - show", :headers - end - - describe "response default headers" do - it_behaves_like "api settings header fields - show", :default_response_headers - end - - describe "response override headers" do - it_behaves_like "api settings header fields - show", :override_response_headers - end - - describe "custom rate limits" do - it "returns embedded custom limit objects" do - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting), - }) - - admin_token_auth(@admin) - get :show, :format => "json", :id => api.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["api"]["settings"]["rate_limits"].length.should eql(1) - rate_limit = data["api"]["settings"]["rate_limits"].first - rate_limit.keys.sort.should eql([ - "id", - "accuracy", - "distributed", - "duration", - "limit", - "limit_by", - "response_headers", - ].sort) - rate_limit["id"].should be_a_uuid - rate_limit["accuracy"].should eql(5000) - rate_limit["distributed"].should eql(true) - rate_limit["duration"].should eql(60000) - rate_limit["limit"].should eql(500) - rate_limit["limit_by"].should eql("ip") - rate_limit["response_headers"].should eql(true) - end - end - end - - describe "POST create" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - let(:params) do - { - :format => "json", - } - end - - describe "servers" do - it_behaves_like "validates nested attributes presence - create", :servers - end - - describe "matching url prefixes" do - it_behaves_like "validates nested attributes presence - create", :url_matches - end - - describe "request headers" do - it_behaves_like "api settings header fields - create", :headers - end - - describe "response default headers" do - it_behaves_like "api settings header fields - create", :default_response_headers - end - - describe "response override headers" do - it_behaves_like "api settings header fields - create", :override_response_headers - end - - it_behaves_like "api settings error data yaml strings", :post, :create - - describe "sort order" do - before(:each) do - Api.delete_all - end - - it "starts the sort_order at 0 and increments by 10,000" do - admin_token_auth(@admin) - - attributes = FactoryGirl.attributes_for(:api) - 3.times do |i| - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["sort_order"].should eql(i * 10_000) - end - end - - it "allows saving when sort_order is pre-set to nil" do - admin_token_auth(@admin) - - expect do - attributes = FactoryGirl.attributes_for(:api, :sort_order => nil) - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["sort_order"].should eql(0) - end.to change { Api.count }.by(1) - end - - it "allows saving when sort_order is pre-set to number" do - admin_token_auth(@admin) - - expect do - attributes = FactoryGirl.attributes_for(:api, :sort_order => 8) - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["sort_order"].should eql(8) - end.to change { Api.count }.by(1) - end - - it "fills in the sort_order when approaching the maximum integer range" do - FactoryGirl.create(:api, :sort_order => 2_147_483_600) - - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api) - - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["sort_order"].should eql(2_147_483_624) - - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["sort_order"].should eql(2_147_483_636) - - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["sort_order"].should eql(2_147_483_642) - end - - it "re-shuffles the sort_order when the maximum integer range will be exceeded" do - api1 = FactoryGirl.create(:api, :sort_order => 2_147_483_000) - api2 = FactoryGirl.create(:api, :sort_order => 2_147_483_645) - - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api) - - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - api3_id = data["api"]["id"] - data["api"]["sort_order"].should eql(2_147_483_646) - - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - api4_id = data["api"]["id"] - data["api"]["sort_order"].should eql(2_147_483_647) - - post :create, :format => "json", :api => attributes - response.status.should eql(201) - data = MultiJson.load(response.body) - api5_id = data["api"]["id"] - data["api"]["sort_order"].should eql(2_147_483_647) - - api1.reload - api2.reload - api3 = Api.find(api3_id) - api4 = Api.find(api4_id) - api5 = Api.find(api5_id) - api1.sort_order.should eql(2_147_483_000) - api2.sort_order.should eql(2_147_483_644) - api3.sort_order.should eql(2_147_483_645) - api4.sort_order.should eql(2_147_483_646) - api5.sort_order.should eql(2_147_483_647) - end - end - end - - describe "PUT update" do - before(:each) do - @admin = FactoryGirl.create(:admin) - @update_api = FactoryGirl.create(:api) - end - - let(:params) do - { - :format => "json", - :id => @update_api.id, - } - end - - describe "servers" do - it_behaves_like "validates nested attributes presence - update", :servers - end - - describe "matching url prefixes" do - it_behaves_like "validates nested attributes presence - update", :url_matches - end - - describe "request headers" do - it_behaves_like "api settings header fields - update", :headers - end - - describe "response default headers" do - it_behaves_like "api settings header fields - update", :default_response_headers - end - - describe "response override headers" do - it_behaves_like "api settings header fields - update", :override_response_headers - end - - describe "custom rate limits" do - it "updates embedded custom rate limit records" do - admin_token_auth(@admin) - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), - ], - }), - }) - - attributes = api.as_json - attributes["settings"]["rate_limits"][0]["limit"] = 50 - attributes["settings"]["rate_limits"][1]["limit"] = 75 - - put :update, :format => "json", :id => api.id, :api => attributes - - api.reload - api.settings.rate_limits.length.should eql(2) - api.settings.rate_limits[0].duration.should eql(5000) - api.settings.rate_limits[0].limit.should eql(50) - api.settings.rate_limits[1].duration.should eql(10000) - api.settings.rate_limits[1].limit.should eql(75) - end - - it "removes embedded custom rate limit records" do - admin_token_auth(@admin) - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), - ], - }), - }) - - attributes = api.as_json - attributes["settings"]["rate_limits"] = [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 1000, :limit => 5), - ] - - put :update, :format => "json", :id => api.id, :api => attributes - - api.reload - api.settings.rate_limits.length.should eql(1) - api.settings.rate_limits[0].duration.should eql(1000) - api.settings.rate_limits[0].limit.should eql(5) - end - end - - it_behaves_like "api settings error data yaml strings", :put, :update - end - - describe "DELETE destroy" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - it "performs soft-deletes" do - admin_token_auth(@admin) - api = FactoryGirl.create(:api) - - delete :destroy, :format => "json", :id => api.id - - Api.where(:id => api.id).first.should eql(nil) - Api.deleted.where(:id => api.id).first.should be_kind_of(Api) - end - end - - describe "PUT move_after" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - - it "moves the sort_order to the beginning when move_after_id is null" do - api1 = FactoryGirl.create(:api) - api2 = FactoryGirl.create(:api) - api3 = FactoryGirl.create(:api) - api4 = FactoryGirl.create(:api) - - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - api3.sort_order.should eql(20_000) - api4.sort_order.should eql(30_000) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api3.id, :move_after_id => nil - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api4.reload - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - api3.sort_order.should eql(-10_000) - api4.sort_order.should eql(30_000) - end - - it "creates a gap of 10,000 when shifting a record to the beginning" do - api1 = FactoryGirl.create(:api, :sort_order => 99) - api2 = FactoryGirl.create(:api) - - api1.sort_order.should eql(99) - api2.sort_order.should eql(10_099) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api2.id, :move_after_id => nil - response.status.should eql(204) - - api1.reload - api2.reload - api1.sort_order.should eql(99) - api2.sort_order.should eql(-9_901) - end - - it "shifts the record into place without touching the surrounding records" do - api1 = FactoryGirl.create(:api) - api2 = FactoryGirl.create(:api) - api3 = FactoryGirl.create(:api) - - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - api3.sort_order.should eql(20_000) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api3.id, :move_after_id => api1.id - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - api3.sort_order.should eql(5_000) - end - - it "doesn't change the sort_order if moving after the record that already precedes it and records are evenly distributed" do - api1 = FactoryGirl.create(:api) - api2 = FactoryGirl.create(:api) - api3 = FactoryGirl.create(:api) - - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - api3.sort_order.should eql(20_000) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api2.id, :move_after_id => api1.id - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - api3.sort_order.should eql(20_000) - end - - it "may change the sort_order if moving after the record that already precedes it and records are not evenly distributed" do - api1 = FactoryGirl.create(:api, :sort_order => 0) - api2 = FactoryGirl.create(:api, :sort_order => 10_000) - api3 = FactoryGirl.create(:api, :sort_order => 100_000) - - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - api3.sort_order.should eql(100_000) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api2.id, :move_after_id => api1.id - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api1.sort_order.should eql(0) - api2.sort_order.should eql(50_000) - api3.sort_order.should eql(100_000) - end - - it "may change the sort_order if moving after the record that already precedes and there is no subsequent record" do - api1 = FactoryGirl.create(:api, :sort_order => 0) - api2 = FactoryGirl.create(:api, :sort_order => 3_000) - - api1.sort_order.should eql(0) - api2.sort_order.should eql(3_000) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api2.id, :move_after_id => api1.id - response.status.should eql(204) - - api1.reload - api2.reload - api1.sort_order.should eql(0) - api2.sort_order.should eql(10_000) - end - - it "reshuffles positive sort orders if moving in between two records that have no gap remaining" do - api1 = FactoryGirl.create(:api, :sort_order => 0) - api2 = FactoryGirl.create(:api, :sort_order => 1) - api3 = FactoryGirl.create(:api, :sort_order => 2) - api4 = FactoryGirl.create(:api, :sort_order => 3) - api5 = FactoryGirl.create(:api, :sort_order => 10) - - api1.sort_order.should eql(0) - api2.sort_order.should eql(1) - api3.sort_order.should eql(2) - api4.sort_order.should eql(3) - api5.sort_order.should eql(10) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api3.id, :move_after_id => api1.id - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api4.reload - api5.reload - api1.sort_order.should eql(-1) - api2.sort_order.should eql(1) - api3.sort_order.should eql(0) - api4.sort_order.should eql(3) - api5.sort_order.should eql(10) - end - - it "reshuffles positive sort orders near integer range if moving in between two records that have no gap remaining" do - api1 = FactoryGirl.create(:api, :sort_order => 2_147_483_640) - api2 = FactoryGirl.create(:api, :sort_order => 2_147_483_645) - api3 = FactoryGirl.create(:api, :sort_order => 2_147_483_646) - api4 = FactoryGirl.create(:api, :sort_order => 2_147_483_647) - - api1.sort_order.should eql(2_147_483_640) - api2.sort_order.should eql(2_147_483_645) - api3.sort_order.should eql(2_147_483_646) - api4.sort_order.should eql(2_147_483_647) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api2.id, :move_after_id => api4.id - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api4.reload - api1.sort_order.should eql(2_147_483_640) - api2.sort_order.should eql(2_147_483_647) - api3.sort_order.should eql(2_147_483_645) - api4.sort_order.should eql(2_147_483_646) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api3.id, :move_after_id => nil - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api4.reload - api1.sort_order.should eql(2_147_483_640) - api2.sort_order.should eql(2_147_483_647) - api3.sort_order.should eql(2_147_473_640) - api4.sort_order.should eql(2_147_483_646) - end - - it "reshuffles negative sort orders if moving in between two records that have no gap remaining" do - api1 = FactoryGirl.create(:api, :sort_order => -10) - api2 = FactoryGirl.create(:api, :sort_order => -9) - api3 = FactoryGirl.create(:api, :sort_order => -8) - api4 = FactoryGirl.create(:api, :sort_order => -7) - api5 = FactoryGirl.create(:api, :sort_order => 0) - - api1.sort_order.should eql(-10) - api2.sort_order.should eql(-9) - api3.sort_order.should eql(-8) - api4.sort_order.should eql(-7) - api5.sort_order.should eql(0) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api3.id, :move_after_id => api1.id - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api4.reload - api5.reload - api1.sort_order.should eql(-10) - api2.sort_order.should eql(-8) - api3.sort_order.should eql(-9) - api4.sort_order.should eql(-7) - api5.sort_order.should eql(0) - end - - it "reshuffles negative sort orders near integer range if moving in between two records that have no gap remaining" do - api1 = FactoryGirl.create(:api, :sort_order => -2_147_483_648) - api2 = FactoryGirl.create(:api, :sort_order => -2_147_483_647) - api3 = FactoryGirl.create(:api, :sort_order => -2_147_483_646) - api4 = FactoryGirl.create(:api, :sort_order => -2_147_483_640) - - api1.sort_order.should eql(-2_147_483_648) - api2.sort_order.should eql(-2_147_483_647) - api3.sort_order.should eql(-2_147_483_646) - api4.sort_order.should eql(-2_147_483_640) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api3.id, :move_after_id => api1.id - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api4.reload - api1.sort_order.should eql(-2_147_483_648) - api2.sort_order.should eql(-2_147_483_646) - api3.sort_order.should eql(-2_147_483_647) - api4.sort_order.should eql(-2_147_483_640) - - admin_token_auth(@admin) - put :move_after, :format => "json", :id => api3.id, :move_after_id => nil - response.status.should eql(204) - - api1.reload - api2.reload - api3.reload - api4.reload - api1.sort_order.should eql(-2_147_483_647) - api2.sort_order.should eql(-2_147_483_646) - api3.sort_order.should eql(-2_147_483_648) - api4.sort_order.should eql(-2_147_483_640) - end - end - - describe "admin permissions" do - shared_examples "admin permitted" do |options| - options ||= {} - - describe "GET index" do - it "includes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should include(record.id) - end - end - - describe "GET show" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data.keys.should eql(["api"]) - end - end - - describe "POST create" do - it "permits access" do - attributes = FactoryGirl.attributes_for(@factory).stringify_keys - admin_token_auth(@admin) - - if(options[:invalid_record]) - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(422) - end.to change { Api.count }.by(0) - else - expect do - post :create, :format => "json", :api => attributes - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["api"]["name"].should_not eql(nil) - data["api"]["name"].should eql(attributes["name"]) - end.to change { Api.count }.by(1) - end - end - end - - describe "PUT update" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["name"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :api => attributes - - if(options[:invalid_record]) - response.status.should eql(422) - else - response.status.should eql(204) - record = Api.find(record.id) - record.name.should_not eql(nil) - record.name.should eql(attributes["name"]) - end - end - end - - describe "DELETE destroy" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - response.status.should eql(204) - end.to change { Api.count }.by(-1) - end - end - end - - shared_examples "admin forbidden" do - describe "GET index" do - it "excludes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should_not include(record.id) - end - end - - describe "GET show" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end - end - - describe "POST create" do - it "forbids access" do - attributes = FactoryGirl.attributes_for(@factory).stringify_keys - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { Api.count }.by(0) - end - end - - describe "PUT update" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["name"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = Api.find(record.id) - record.name.should_not eql(nil) - record.name.should_not eql(attributes["name"]) - end - end - - describe "DELETE destroy" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { Api.count }.by(0) - end - end - end - - describe "localhost/google api (single prefix)" do - before(:each) do - @factory = :google_api - end - - it_behaves_like "admin permissions", :required_permissions => ["backend_manage"] - end - - describe "localhost/google and localhost/extra api (multi prefix)" do - before(:each) do - @factory = :google_extra_url_match_api - @google_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope)) - @extra_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:extra_api_scope)) - end - - describe "superuser" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* and localhost/extra* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - @extra_api_scope, - ]), - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/extra* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @extra_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - end - - describe "incomplete localhost api without any url matches" do - before(:each) do - @factory = :empty_url_matches_api - end - - describe "superuser" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - it_behaves_like "admin permitted", :invalid_record => true - end - - describe "localhost/* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:localhost_root_admin_group), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:google_admin_group), - ]) - end - it_behaves_like "admin forbidden" - end - end - - it "returns a list of scopes the admin does have access to in the error response" do - admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:api_scope, :path_prefix => "/a")), - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:api_scope, :path_prefix => "/c")), - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:api_scope, :path_prefix => "/b")), - ]), - ]) - admin_token_auth(admin) - attributes = FactoryGirl.attributes_for(:google_api) - - expect do - post :create, :format => "json", :api => attributes - response.status.should eql(403) - data = MultiJson.load(response.body) - data["errors"].should eql([ - { - "code" => "FORBIDDEN", - "message" => "You are not authorized to perform this action. You are only authorized to perform actions for APIs in the following areas:\n\n- localhost/a\n- localhost/b\n- localhost/c\n\nContact your API Umbrella administrator if you need access to new APIs.", - } - ]) - end.to_not change { Api.count } - end - - it "prevents admins from updating apis within its scope to contain routing outside its scope" do - admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = record.serializable_hash - attributes["name"] += rand(999_999).to_s - attributes["url_matches"] << FactoryGirl.attributes_for(:api_url_match, :frontend_prefix => "/foo", :backend_prefix => "/") - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = Api.find(record.id) - record.name.should_not eql(attributes["name"]) - record.url_matches.length.should eql(1) - end - - it "prevents limited admins from updating forbidden apis to only contain routes the admin does have permissions to" do - record = FactoryGirl.create(:api, :url_matches => [ - FactoryGirl.attributes_for(:api_url_match, :frontend_prefix => "/yahoo", :backend_prefix => "/"), - ]) - - admin = FactoryGirl.create(:google_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["url_matches"][0]["frontend_prefix"] = "/google" - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = Api.find(record.id) - record.url_matches[0].frontend_prefix.should eql("/yahoo") - end - - describe "role permissions" do - it "allows superuser admins to assign any roles" do - FactoryGirl.create(:google_api) - FactoryGirl.create(:yahoo_api) - existing_roles = ApiUserRole.all - existing_roles.should eql(["google-write", "yahoo-write"]) - - admin = FactoryGirl.create(:admin) - admin_token_auth(admin) - - record = FactoryGirl.create(:api) - attributes = FactoryGirl.attributes_for(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "test-write", - "google-write", - "yahoo-write", - "new-write", - "new-write#{rand(999_999)}", - ], - }), - :sub_settings => [ - FactoryGirl.attributes_for(:api_sub_setting, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "test-write", - "google-write", - "yahoo-write", - "new-write", - "new-write#{rand(999_999)}", - ], - }) - }), - ], - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(204) - record = Api.find(record.id) - record.settings.required_roles.should eql(attributes[:settings][:required_roles]) - record.sub_settings[0].settings.required_roles.should eql(attributes[:sub_settings][0][:settings][:required_roles]) - end - - it "allows limited admins to assign any unused role" do - existing_roles = ApiUserRole.all - existing_roles.should eql([]) - - admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = FactoryGirl.attributes_for(:google_api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "new-settings-role#{rand(999_999)}", - ], - }), - :sub_settings => [ - FactoryGirl.attributes_for(:api_sub_setting, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "new-sub-settings-role#{rand(999_999)}", - ], - }), - }), - ], - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(204) - record = Api.find(record.id) - record.settings.required_roles.should eql(attributes[:settings][:required_roles]) - record.sub_settings[0].settings.required_roles.should eql(attributes[:sub_settings][0][:settings][:required_roles]) - end - - it "allows limited admins to assign an existing role that exists within its scope" do - FactoryGirl.create(:google_api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "google2-write", - ], - }), - }) - existing_roles = ApiUserRole.all - existing_roles.should eql(["google-write", "google2-write"]) - - admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = FactoryGirl.attributes_for(:google_api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "google2-write", - ], - }), - :sub_settings => [ - FactoryGirl.attributes_for(:api_sub_setting, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "google2-write", - ], - }), - }), - ], - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(204) - record = Api.find(record.id) - record.settings.required_roles.should eql(attributes[:settings][:required_roles]) - record.sub_settings[0].settings.required_roles.should eql(attributes[:sub_settings][0][:settings][:required_roles]) - end - - it "forbids limited admins from assigning an existing role that exists outside its scope at the settings level" do - FactoryGirl.create(:yahoo_api) - existing_roles = ApiUserRole.all - existing_roles.should eql(["yahoo-write"]) - - admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = FactoryGirl.attributes_for(:google_api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "yahoo-write", - ], - }), - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - record = Api.find(record.id) - record.roles.should_not include("yahoo-write") - end - - it "forbids limited admins from assigning an existing role that exists outside its scope at the sub-settings level" do - FactoryGirl.create(:yahoo_api) - existing_roles = ApiUserRole.all - existing_roles.should eql(["yahoo-write"]) - - admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = FactoryGirl.attributes_for(:google_api, { - :sub_settings => [ - FactoryGirl.attributes_for(:api_sub_setting, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "yahoo-write", - ], - }), - }), - ], - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - record = Api.find(record.id) - record.roles.should_not include("yahoo-write") - end - - it "forbids limited admins from assigning an existing role that exists in an api the admin only has partial access to" do - FactoryGirl.create(:google_extra_url_match_api) - existing_roles = ApiUserRole.all - existing_roles.should eql(["google-extra-write", "google-write"]) - - admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = FactoryGirl.attributes_for(:google_api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "google-extra-write", - ], - }), - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - record = Api.find(record.id) - record.roles.should_not include("google-extra-write") - end - - it "forbids limited admins from assigning a new role beginning with 'api-umbrella'" do - existing_roles = ApiUserRole.all - existing_roles.should eql([]) - - admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = FactoryGirl.attributes_for(:google_api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "api-umbrella#{rand(999_999)}", - ], - }), - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - record = Api.find(record.id) - record.roles.should_not include(attributes[:settings][:required_roles][0]) - end - - it "allows superuser admins to assign a new role beginning with 'api-umbrella'" do - existing_roles = ApiUserRole.all - existing_roles.should eql([]) - - admin = FactoryGirl.create(:admin) - admin_token_auth(admin) - - record = FactoryGirl.create(:google_api) - attributes = FactoryGirl.attributes_for(:google_api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :required_roles => [ - "api-umbrella#{rand(999_999)}", - ], - }), - }) - put :update, :format => "json", :id => record.id, :api => attributes - - response.status.should eql(204) - record = Api.find(record.id) - record.settings.required_roles.should eql(attributes[:settings][:required_roles]) - end - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/base_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/base_controller_spec.rb deleted file mode 100644 index 60e80113f..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/base_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Api::V1::BaseController do - -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/config_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/config_controller_spec.rb deleted file mode 100644 index 122d3746d..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/config_controller_spec.rb +++ /dev/null @@ -1,698 +0,0 @@ -require 'spec_helper' - -describe Api::V1::ConfigController do - before(:all) do - @admin = FactoryGirl.create(:admin) - @google_admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_publish_permission)]) - @unauthorized_google_admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :backend_manage_permission)]) - end - - before(:each) do - Api.delete_all - ConfigVersion.delete_all - end - - describe "GET pending_changes" do - it "returns apis grouped into categories" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - data["config"].should be_kind_of(Hash) - data["config"]["apis"].should be_kind_of(Hash) - data["config"]["apis"]["deleted"].should be_kind_of(Array) - data["config"]["apis"]["identical"].should be_kind_of(Array) - data["config"]["apis"]["modified"].should be_kind_of(Array) - data["config"]["apis"]["new"].should be_kind_of(Array) - end - - describe "yaml output" do - before(:each) do - @api = FactoryGirl.create(:api, :name => "YAML Test") - end - - it "omits the yaml separator" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_data = data["config"]["apis"]["new"].first - api_data["pending_yaml"].should_not include("---") - end - - it "omits fields in the yaml that exist in the json output but don't matter for diff purposes" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_data = data["config"]["apis"]["new"].first - %w(_id version created_by created_at updated_at updated_by).each do |field| - api_data["pending"][field].present?.should eql(true) - api_data["pending_yaml"].should_not include(field) - end - end - - it "returns the yaml sorted in alphabetical order by key for better diff purposes" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_data = data["config"]["apis"]["new"].first - - yaml_lines = api_data["pending_yaml"].split("\n") - yaml_keys = yaml_lines.map { |line| line.gsub(/:.*/, "") } - yaml_keys.should eql([ - "backend_host", - "backend_protocol", - "balance_algorithm", - "frontend_host", - "name", - "servers", - "- host", - " port", - "sort_order", - "url_matches", - "- backend_prefix", - " frontend_prefix", - ]) - end - end - - describe "deleted" do - before(:each) do - @api = FactoryGirl.create(:api) - ConfigVersion.publish!(ConfigVersion.pending_config) - @api.destroy - end - - it "considers apis deleted when they are deleted after the last config publish" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - data["config"]["apis"]["deleted"].length.should eql(1) - data["config"]["apis"]["identical"].length.should eql(0) - data["config"]["apis"]["modified"].length.should eql(0) - data["config"]["apis"]["new"].length.should eql(0) - end - - it "includes the expected output for each deleted api" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_data = data["config"]["apis"]["deleted"].first - api_data["mode"].should eql("deleted") - api_data["id"].should eql(@api.id) - api_data["name"].should eql(@api.name) - api_data["active"]["_id"].should eql(@api.id) - api_data["active_yaml"].should include("name: #{@api.name}") - api_data["pending"].should eql(nil) - api_data["pending_yaml"].should eql("") - end - end - - describe "identical" do - before(:each) do - @api = FactoryGirl.create(:api) - ConfigVersion.publish!(ConfigVersion.pending_config) - end - - it "considers apis identical when there are now changes after the last config publish" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - data["config"]["apis"]["deleted"].length.should eql(0) - data["config"]["apis"]["identical"].length.should eql(1) - data["config"]["apis"]["modified"].length.should eql(0) - data["config"]["apis"]["new"].length.should eql(0) - end - - it "includes the expected output for each identical api" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_data = data["config"]["apis"]["identical"].first - api_data["mode"].should eql("identical") - api_data["id"].should eql(@api.id) - api_data["name"].should eql(@api.name) - api_data["active"]["_id"].should eql(@api.id) - api_data["active_yaml"].should include("name: #{@api.name}") - api_data["pending"]["_id"].should eql(@api.id) - api_data["pending_yaml"].should include("name: #{@api.name}") - end - end - - describe "modified" do - before(:each) do - @api = FactoryGirl.create(:api, :name => "Before") - ConfigVersion.publish!(ConfigVersion.pending_config) - @api.update_attribute(:name, "After") - end - - it "considers apis modified when they are modified after the last config publish" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - data["config"]["apis"]["deleted"].length.should eql(0) - data["config"]["apis"]["identical"].length.should eql(0) - data["config"]["apis"]["modified"].length.should eql(1) - data["config"]["apis"]["new"].length.should eql(0) - end - - it "includes the expected output for each modified api" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_data = data["config"]["apis"]["modified"].first - api_data["mode"].should eql("modified") - api_data["id"].should eql(@api.id) - api_data["name"].should eql("After") - api_data["active"]["name"].should eql("Before") - api_data["active_yaml"].should include("name: Before") - api_data["pending"]["name"].should eql("After") - api_data["pending_yaml"].should include("name: After") - end - end - - describe "new" do - before(:each) do - @api = FactoryGirl.create(:api) - end - - it "considers all apis new when no previous config has been published" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - data["config"]["apis"]["deleted"].length.should eql(0) - data["config"]["apis"]["identical"].length.should eql(0) - data["config"]["apis"]["modified"].length.should eql(0) - data["config"]["apis"]["new"].length.should eql(1) - end - - it "considers apis new when they are created after the last config publish" do - ConfigVersion.publish!(ConfigVersion.pending_config) - @google_api = FactoryGirl.create(:google_api) - - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - data["config"]["apis"]["deleted"].length.should eql(0) - data["config"]["apis"]["identical"].length.should eql(1) - data["config"]["apis"]["modified"].length.should eql(0) - data["config"]["apis"]["new"].length.should eql(1) - end - - it "includes the expected output for each new api" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_data = data["config"]["apis"]["new"].first - api_data["mode"].should eql("new") - api_data["id"].should eql(@api.id) - api_data["name"].should eql(@api.name) - api_data["active"].should eql(nil) - api_data["active_yaml"].should eql("") - api_data["pending"]["_id"].should eql(@api.id) - api_data["pending_yaml"].should include("name: #{@api.name}") - end - end - - describe "admin permissions" do - before(:each) do - @api = FactoryGirl.create(:api) - @google_api = FactoryGirl.create(:google_api) - @google_extra_url_match_api = FactoryGirl.create(:google_extra_url_match_api) - @yahoo_api = FactoryGirl.create(:yahoo_api) - - ConfigVersion.publish!(ConfigVersion.pending_config) - end - - it "includes all apis for superuser admins" do - admin_token_auth(@admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_ids = data["config"]["apis"]["identical"].map { |api| api["pending"]["_id"] } - api_ids.should include(@api.id) - api_ids.should include(@google_api.id) - api_ids.should include(@google_extra_url_match_api.id) - api_ids.should include(@yahoo_api.id) - end - - it "includes apis the admin has access to" do - admin_token_auth(@google_admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_ids = data["config"]["apis"]["identical"].map { |api| api["pending"]["_id"] } - api_ids.should include(@google_api.id) - end - - it "excludes apis the admin does not have access to" do - admin_token_auth(@google_admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_ids = data["config"]["apis"]["identical"].map { |api| api["pending"]["_id"] } - api_ids.should_not include(@yahoo_api.id) - end - - it "excludes apis the admin only has partial access to" do - admin_token_auth(@google_admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_ids = data["config"]["apis"]["identical"].map { |api| api["pending"]["_id"] } - api_ids.should_not include(@google_extra_url_match_api.id) - end - - it "excludes all apis for admins without proper access" do - admin_token_auth(@unauthorized_google_admin) - get :pending_changes, :format => "json" - - data = MultiJson.load(response.body) - api_ids = data["config"]["apis"]["identical"].map { |api| api["pending"]["_id"] } - api_ids.length.should eql(0) - end - end - end - - describe "POST publish" do - it "publishes changes when there was no pre-existing published config" do - ConfigVersion.count.should eql(0) - - api = FactoryGirl.create(:api) - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(1) - active_config = ConfigVersion.active_config - active_config["apis"].length.should eql(1) - end - - it "publishes changes when there was a pre-existing published config" do - FactoryGirl.create(:api) - ConfigVersion.publish!(ConfigVersion.pending_config) - ConfigVersion.count.should eql(1) - - api = FactoryGirl.create(:api) - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(2) - active_config = ConfigVersion.active_config - active_config["apis"].length.should eql(2) - end - - it "combines the newly published config and in sorted order" do - api1 = FactoryGirl.create(:api, :sort_order => 40) - api2 = FactoryGirl.create(:api, :sort_order => 15) - ConfigVersion.publish!(ConfigVersion.pending_config) - ConfigVersion.count.should eql(1) - - api3 = FactoryGirl.create(:api, :sort_order => 90) - api4 = FactoryGirl.create(:api, :sort_order => 1) - api5 = FactoryGirl.create(:api, :sort_order => 50) - api6 = FactoryGirl.create(:api, :sort_order => 20) - - config = { - :apis => { - api3.id => { :publish => "1" }, - api4.id => { :publish => "1" }, - api5.id => { :publish => "1" }, - api6.id => { :publish => "1" }, - } - } - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - active_config = ConfigVersion.active_config - active_config["apis"].map { |api| api["_id"] }.should eql([ - api4.id, - api2.id, - api6.id, - api1.id, - api5.id, - api3.id, - ]) - end - - it "only publishes the selected apis" do - api1 = FactoryGirl.create(:api, :name => "Before") - ConfigVersion.publish!(ConfigVersion.pending_config) - - api1.update_attribute(:name, "After") - api2 = FactoryGirl.create(:api) - api3 = FactoryGirl.create(:api) - - config = { - :apis => { - api2.id => { :publish => "1" }, - api3.id => { :publish => "0" }, - } - } - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - active_config = ConfigVersion.active_config - active_config["apis"].map { |api| api["_id"] }.sort.should eql([ - api1.id, - api2.id, - ].sort) - - api1_config = active_config["apis"].detect { |api| api["_id"] == api1.id } - api1_config["name"].should eql("Before") - end - - it "does nothing when no changes are submited" do - api1 = FactoryGirl.create(:api, :name => "Before") - initial = ConfigVersion.publish!(ConfigVersion.pending_config) - initial.reload - - api1.update_attribute(:name, "After") - FactoryGirl.create(:api) - FactoryGirl.create(:api) - - admin_token_auth(@admin) - post :publish, :format => "json", :config => {} - - active = ConfigVersion.active - active.id.should be_kind_of(Moped::BSON::ObjectId) - active.id.should eql(initial.id) - active.version.should be_kind_of(Time) - active.version.should eql(initial.version) - active_config = active.config - active_config["apis"].map { |api| api["_id"] }.sort.should eql([ - api1.id, - ].sort) - - api1_config = active_config["apis"].detect { |api| api["_id"] == api1.id } - api1_config["name"].should eql("Before") - end - - describe "admin permissions" do - before(:each) do - @api = FactoryGirl.create(:api) - @google_api = FactoryGirl.create(:google_api) - @google_extra_url_match_api = FactoryGirl.create(:google_extra_url_match_api) - @yahoo_api = FactoryGirl.create(:yahoo_api) - end - - it "allows superusers to publish any api" do - config = { - :apis => { - @api.id => { :publish => "1" }, - @google_api.id => { :publish => "1" }, - @google_extra_url_match_api.id => { :publish => "1" }, - @yahoo_api.id => { :publish => "1" }, - } - } - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - response.status.should eql(201) - active_config = ConfigVersion.active_config - active_config["apis"].length.should eql(4) - active_config["apis"].map { |api| api["_id"] }.sort.should eql([ - @api.id, - @google_api.id, - @google_extra_url_match_api.id, - @yahoo_api.id, - ].sort) - end - - it "allows limited admins to publish apis they have access to" do - config = { - :apis => { - @google_api.id => { :publish => "1" }, - } - } - - admin_token_auth(@google_admin) - post :publish, :format => "json", :config => config - - response.status.should eql(201) - active_config = ConfigVersion.active_config - active_config["apis"].length.should eql(1) - active_config["apis"].first["_id"].should eql(@google_api.id) - end - - it "forbids limited admins from publishing apis they do not have access to" do - config = { - :apis => { - @yahoo_api.id => { :publish => "1" }, - } - } - - admin_token_auth(@google_admin) - post :publish, :format => "json", :config => config - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - ConfigVersion.active_config.should eql(nil) - end - - it "forbids limited admins from publishing apis they only have partial access to" do - config = { - :apis => { - @google_extra_url_match_api.id => { :publish => "1" }, - } - } - - admin_token_auth(@google_admin) - post :publish, :format => "json", :config => config - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - ConfigVersion.active_config.should eql(nil) - end - - it "forbids admins with proper access" do - config = { - :apis => { - @google_api.id => { :publish => "1" }, - } - } - - admin_token_auth(@unauthorized_google_admin) - post :publish, :format => "json", :config => config - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - ConfigVersion.active_config.should eql(nil) - end - end - - describe "transitionary https" do - ["transition_return_error", "transition_return_redirect"].each do |mode| - it "sets the transition timestamp for #{mode.inspect} mode when publishing" do - api = FactoryGirl.create(:api, { - :settings => { - :require_https => mode, - }, - }) - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - api.settings.require_https_transition_start_at.should eql(nil) - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(1) - active_config = ConfigVersion.active_config - - api.reload - api.settings.require_https_transition_start_at.should be_kind_of(Time) - active_config["apis"][0]["settings"]["require_https_transition_start_at"].should be_kind_of(Time) - end - - it "sets the transition timestamp for #{mode.inspect} mode in sub-settings when publishing" do - api = FactoryGirl.create(:api, { - :sub_settings => [ - FactoryGirl.attributes_for(:api_sub_setting, { - :settings_attributes => FactoryGirl.attributes_for(:api_setting, { - :require_https => mode, - }) - }), - ], - }) - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - api.sub_settings[0].settings.require_https_transition_start_at.should eql(nil) - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(1) - active_config = ConfigVersion.active_config - - api.reload - api.sub_settings[0].settings.require_https_transition_start_at.should be_kind_of(Time) - active_config["apis"][0]["sub_settings"][0]["settings"]["require_https_transition_start_at"].should be_kind_of(Time) - end - - it "does not change existing transition timestamp for #{mode.inspect} mode when publishing" do - timestamp = Time.parse("2015-01-16T06:06:28.816Z") - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :require_https => mode, - :require_https_transition_start_at => timestamp, - }), - }) - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - api.settings.require_https_transition_start_at.should eql(timestamp) - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(1) - active_config = ConfigVersion.active_config - - api.reload - api.settings.require_https_transition_start_at.should eql(timestamp) - active_config["apis"][0]["settings"]["require_https_transition_start_at"].should eql(timestamp) - end - - it "does not change existing transition timestamp for #{mode.inspect} mode if the mode changes are made without publishing" do - timestamp = Time.parse("2015-01-16T06:06:28.816Z") - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :require_https => mode, - :require_https_transition_start_at => timestamp, - }), - }) - - api.settings.require_https = "required_return_error" - api.save! - - api.settings.require_https = "required_return_redirect" - api.save! - - api.settings.require_https = "optional" - api.save! - - api.settings.require_https = nil - api.save! - - api.settings.require_https = mode - api.save! - - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - api.settings.require_https_transition_start_at.should eql(timestamp) - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(1) - active_config = ConfigVersion.active_config - - api.reload - api.settings.require_https_transition_start_at.should eql(timestamp) - active_config["apis"][0]["settings"]["require_https_transition_start_at"].should eql(timestamp) - end - end - - ["required_return_error", "required_return_redirect", "optional", nil].each do |mode| - it "unsets the transition timestamp for #{mode.inspect} mode when publishing" do - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.attributes_for(:api_setting, { - :require_https => mode, - :require_https_transition_start_at => Time.now, - }), - }) - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - api.settings.require_https_transition_start_at.should be_kind_of(Time) - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(1) - active_config = ConfigVersion.active_config - - api.reload - api.settings.require_https_transition_start_at.should eql(nil) - active_config["apis"][0]["settings"]["require_https_transition_start_at"].should eql(nil) - end - - it "unsets the transition timestamp for #{mode.inspect} mode in sub-settings when publishing" do - api = FactoryGirl.create(:api, { - :sub_settings => [ - FactoryGirl.attributes_for(:api_sub_setting, { - :settings_attributes => FactoryGirl.attributes_for(:api_setting, { - :require_https => mode, - :require_https_transition_start_at => Time.now, - }) - }), - ], - }) - config = { - :apis => { - api.id => { :publish => "1" }, - } - } - - api.sub_settings[0].settings.require_https_transition_start_at.should be_kind_of(Time) - - admin_token_auth(@admin) - post :publish, :format => "json", :config => config - - ConfigVersion.count.should eql(1) - active_config = ConfigVersion.active_config - - api.reload - api.sub_settings[0].settings.require_https_transition_start_at.should eql(nil) - active_config["apis"][0]["sub_settings"][0]["settings"]["require_https_transition_start_at"].should eql(nil) - end - end - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/users_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/users_controller_spec.rb deleted file mode 100644 index 15e23a60d..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/users_controller_spec.rb +++ /dev/null @@ -1,1364 +0,0 @@ -require "spec_helper" - -describe Api::V1::UsersController do - before(:each) do - DatabaseCleaner.clean - - @admin = FactoryGirl.create(:admin) - @google_admin = FactoryGirl.create(:limited_admin, :groups => [FactoryGirl.create(:google_admin_group, :user_view_permission, :user_manage_permission)]) - - @api = FactoryGirl.create(:api) - @google_api = FactoryGirl.create(:google_api) - @google_extra_url_match_api = FactoryGirl.create(:google_extra_url_match_api) - @yahoo_api = FactoryGirl.create(:yahoo_api) - end - - shared_examples "admin token access" do |method, action| - it "forbids access without an admin token" do - send(method, action, params) - response.status.should eql(401) - data = MultiJson.load(response.body) - data.should eql({ - "error" => "You need to sign in or sign up before continuing.", - }) - end - - it "allows access with an admin token" do - admin_token_auth(@admin) - send(method, action, params) - response.status.should eql(success_response_status) - end - end - - shared_examples "no api key role access" do |method, action| - it "forbids access with an api key" do - request.env["HTTP_X_API_ROLES"] = "api-umbrella-key-creator" - send(method, action, params) - response.status.should eql(401) - data = MultiJson.load(response.body) - data.should eql({ - "error" => "You need to sign in or sign up before continuing.", - }) - end - end - - shared_examples "api key role access" do |method, action| - it "forbids access without the special role" do - request.env["HTTP_X_API_ROLES"] = "api-umbrella-key-creator-bogus" - send(method, action, params) - response.status.should eql(401) - data = MultiJson.load(response.body) - data.should eql({ - "error" => "You need to sign in or sign up before continuing.", - }) - end - - it "allows access with a special role" do - request.env["HTTP_X_API_ROLES"] = "test1,api-umbrella-key-creator,test2" - send(method, action, params) - response.status.should eql(success_response_status) - end - end - - shared_examples "admin role permissions" do |method, action| - it "allows superuser admins to assign any roles" do - existing_roles = ApiUserRole.all - existing_roles.should include("google-write") - existing_roles.should include("yahoo-write") - existing_roles.should_not include("new-write") - - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "google-write", - "yahoo-write", - "new-write", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(success_response_status) - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.roles.should eql(attributes[:roles]) - end.to change { ApiUser.count }.by(success_record_change_count) - end - - it "allows limited admins to assign any unused role" do - admin_token_auth(@google_admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "new-role#{rand(999_999)}", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(success_response_status) - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.roles.should eql(attributes[:roles]) - end.to change { ApiUser.count }.by(success_record_change_count) - end - - it "allows limited admins to assign an existing role that exists within its scope" do - existing_roles = ApiUserRole.all - existing_roles.should include("google-write") - - admin_token_auth(@google_admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "google-write", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(success_response_status) - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.roles.should eql(attributes[:roles]) - end.to change { ApiUser.count }.by(success_record_change_count) - end - - it "forbids limited admins from assigning an existing role that exists outside its scope at the settings level" do - existing_roles = ApiUserRole.all - existing_roles.should include("yahoo-write") - - admin_token_auth(@google_admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "yahoo-write", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to_not change { ApiUser.count } - end - - it "forbids limited admins from assigning an existing role that exists in an api the admin only has partial access to" do - existing_roles = ApiUserRole.all - existing_roles.should include("google-extra-write") - - admin_token_auth(@google_admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "google-extra-write", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to_not change { ApiUser.count } - end - - it "allows limited admins to assign the 'api-umbrella-key-creator' role" do - admin_token_auth(@google_admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "api-umbrella-key-creator", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(success_response_status) - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.roles.should eql(attributes[:roles]) - end.to change { ApiUser.count }.by(success_record_change_count) - end - - it "forbids limited admins from assigning other new roles beginning with 'api-umbrella'" do - admin_token_auth(@google_admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "api-umbrella#{rand(999_999)}", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to_not change { ApiUser.count } - end - - it "allows superuser admins to assign other new roles beginning with 'api-umbrella'" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api_user, { - :roles => [ - "api-umbrella#{rand(999_999)}", - ], - }) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(success_response_status) - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.roles.should eql(attributes[:roles]) - end.to change { ApiUser.count }.by(success_record_change_count) - end - end - - shared_examples "server-side timestamps" do |method, action| - it "maintains the `ts` field with a server-side last modified timestamp" do - admin_token_auth(@admin) - attributes = FactoryGirl.attributes_for(:api_user) - - expect do - send(method, action, params.merge(:user => attributes)) - response.status.should eql(success_response_status) - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.ts.should be_kind_of(Moped::BSON::Timestamp) - user.ts.seconds.should be_within(2).of(Time.now.to_i) - user.ts.increment.should be_kind_of(Numeric) - end.to change { ApiUser.count }.by(success_record_change_count) - end - end - - describe "GET index" do - it "paginates results" do - FactoryGirl.create_list(:api_user, 10) - - user_count = ApiUser.count - user_count.should be >= 10 - - admin_token_auth(@admin) - get :index, :format => "json", :length => 2 - - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(user_count) - data["recordsFiltered"].should eql(user_count) - data["data"].length.should eql(2) - end - - it "only returns the api key preview and not the full api key" do - api_user = FactoryGirl.create(:api_user) - - admin_token_auth(@admin) - get(:index, :format => "json") - response.status.should eql(200) - - data = MultiJson.load(response.body) - user = data["data"].find { |u| u["id"] == api_user.id } - user.keys.should_not include("api_key") - user.keys.should_not include("api_key_hides_at") - user["api_key_preview"].should eql("#{api_user.api_key[0, 6]}...") - end - - describe "search" do - shared_examples "wildcard, case-insensitive search over field" do |field, value, search| - it "matches on #{field}" do - api_user = FactoryGirl.create(:api_user, field => value) - - admin_token_auth(@admin) - get(:index, :format => "json", :search => { :value => search }) - response.status.should eql(200) - - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(1) - data["data"].length.should eql(1) - data["data"].first["id"].should eql(api_user.id) - end - - it "excludes non-matching results on #{field}" do - FactoryGirl.create(:api_user, field => value) - - admin_token_auth(@admin) - get(:index, :format => "json", :search => { :value => "#{search}extra" }) - response.status.should eql(200) - - data = MultiJson.load(response.body) - data["recordsTotal"].should eql(0) - data["data"].length.should eql(0) - end - end - - it_behaves_like "wildcard, case-insensitive search over field", :first_name, "FirstNameSearchTest", "IRSTNAMEsearchT" - it_behaves_like "wildcard, case-insensitive search over field", :last_name, "LastNameSearchTest", "astnamesearcht" - it_behaves_like "wildcard, case-insensitive search over field", :email, "EmailSearchTest@example.com", "mailsearchtest@example" - it_behaves_like "wildcard, case-insensitive search over field", :api_key, "API_KEY_SEARCH_TEST", "_key_search_tes" - it_behaves_like "wildcard, case-insensitive search over field", :registration_source, "RegistrationSourceSearchTest", "registrationsourcesearchtest" - it_behaves_like "wildcard, case-insensitive search over field", :registration_source, "RegistrationSourceSearchTest", "registrationsourcesearchtest" - it_behaves_like "wildcard, case-insensitive search over field", :roles, ["RoleSearchTest1", "RoleSearchTest2", "RoleSearchTest3"], "olesearchtest3" - it_behaves_like "wildcard, case-insensitive search over field", :roles, "381f2ad2-493b-4750-994d-a046fa6eae70", "994D-A046" - end - end - - describe "GET show" do - before(:each) do - @api_user = FactoryGirl.create(:api_user, :created_by => @admin.id) - end - - shared_examples "allowed to view full api key" do - it "returns api key preview, the full api key, and a hides at date" do - get(:show, :id => api_user.id, :format => "json") - response.status.should eql(200) - - data = MultiJson.load(response.body) - data["user"]["api_key"].should eql(api_user.api_key) - data["user"]["api_key_hides_at"].should eql((api_user.created_at + 2.weeks).iso8601) - data["user"]["api_key_preview"].should eql("#{api_user.api_key[0, 6]}...") - end - end - - shared_examples "not allowed to view full api key" do - it "returns only the api key preview, but not the full api key or hides at date" do - get(:show, :id => api_user.id, :format => "json") - response.status.should eql(200) - - data = MultiJson.load(response.body) - data["user"].keys.should_not include("api_key") - data["user"].keys.should_not include("api_key_hides_at") - data["user"]["api_key_preview"].should eql("#{api_user.api_key[0, 6]}...") - end - end - - let(:params) do - { :format => "json", :id => @api_user.id } - end - let(:success_response_status) { 200 } - - it_behaves_like "admin token access", :get, :show - it_behaves_like "no api key role access", :get, :show - - it "contains the user response" do - admin_token_auth(@admin) - get :show, params - - data = MultiJson.load(response.body) - data.keys.sort.should eql([ - "user", - ]) - - expected_keys = [ - "api_key", - "api_key_hides_at", - "api_key_preview", - "created_at", - "creator", - "email", - "email_verified", - "enabled", - "first_name", - "id", - "last_name", - "registration_ip", - "registration_origin", - "registration_referer", - "registration_source", - "registration_user_agent", - "roles", - "settings", - "throttle_by_ip", - "updated_at", - "updater", - "use_description", - ] - - if(ApiUser.fields.include?("website")) - expected_keys << "website" - end - - data["user"].keys.sort.should eql(expected_keys.sort) - end - - describe "rate limits" do - it "returns embedded custom limit objects" do - user = FactoryGirl.create(:custom_rate_limit_api_user) - - admin_token_auth(@admin) - get :show, :format => "json", :id => user.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data["user"]["settings"]["rate_limits"].length.should eql(1) - rate_limit = data["user"]["settings"]["rate_limits"].first - rate_limit.keys.sort.should eql([ - "id", - # Legacy _id field we never meant to return (everything else returns - # just "id"), but we accidentally did in this embedded case. Keep - # returning for backwards compatibility, but should remove for V2 of - # APIs. - "_id", - "accuracy", - "distributed", - "duration", - "limit", - "limit_by", - "response_headers", - ].sort) - rate_limit["id"].should be_a_uuid - rate_limit["_id"].should eql(rate_limit["id"]) - rate_limit["accuracy"].should eql(5000) - rate_limit["distributed"].should eql(true) - rate_limit["duration"].should eql(60000) - rate_limit["limit"].should eql(500) - rate_limit["limit_by"].should eql("ip") - rate_limit["response_headers"].should eql(true) - end - end - - describe "api key" do - describe "superuser admin is logged in" do - let(:current_admin) { FactoryGirl.create(:admin) } - login_admin - - describe "accounts without roles" do - describe "new accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => (Time.now - 2.weeks + 5.minutes), :roles => nil) } - it_behaves_like "allowed to view full api key" - end - - describe "old accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => (Time.now - 2.weeks - 5.minutes), :roles => nil) } - it_behaves_like "allowed to view full api key" - end - - describe "new accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks + 5.minutes, :roles => nil) } - it_behaves_like "allowed to view full api key" - end - - describe "old accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks - 5.minutes, :roles => nil) } - it_behaves_like "allowed to view full api key" - end - end - - describe "accounts with roles" do - describe "new accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => Time.now - 2.weeks + 5.minutes, :roles => ["foo"]) } - it_behaves_like "allowed to view full api key" - end - - describe "old accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => Time.now - 2.weeks - 5.minutes, :roles => ["foo"]) } - it_behaves_like "allowed to view full api key" - end - - describe "new accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks + 5.minutes, :roles => ["foo"]) } - it_behaves_like "allowed to view full api key" - end - - describe "old accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks - 5.minutes, :roles => ["foo"]) } - it_behaves_like "allowed to view full api key" - end - end - end - - describe "limited admin is logged in" do - let(:current_admin) { FactoryGirl.create(:limited_admin) } - login_admin - - describe "accounts without roles" do - describe "new accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => (Time.now - 2.weeks + 5.minutes), :roles => nil) } - it_behaves_like "allowed to view full api key" - end - - describe "old accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => (Time.now - 2.weeks - 5.minutes), :roles => nil) } - it_behaves_like "not allowed to view full api key" - end - - describe "new accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks + 5.minutes, :roles => nil) } - it_behaves_like "allowed to view full api key" - end - - describe "old accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks - 5.minutes, :roles => nil) } - it_behaves_like "not allowed to view full api key" - end - end - - describe "accounts with roles" do - describe "new accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => Time.now - 2.weeks + 5.minutes, :roles => ["foo"]) } - it_behaves_like "allowed to view full api key" - end - - describe "old accounts they created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => current_admin.id, :created_at => Time.now - 2.weeks - 5.minutes, :roles => ["foo"]) } - it_behaves_like "not allowed to view full api key" - end - - describe "new accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks + 5.minutes, :roles => ["foo"]) } - it_behaves_like "not allowed to view full api key" - end - - describe "old accounts other admins created" do - let(:api_user) { FactoryGirl.create(:api_user, :created_by => @admin.id, :created_at => Time.now - 2.weeks - 5.minutes, :roles => ["foo"]) } - it_behaves_like "not allowed to view full api key" - end - end - end - end - end - - describe "POST create" do - let(:params) do - { - :format => "json", - :user => { - :first_name => "Mr", - :last_name => "Potato", - :email => "potato@example.com", - :use_description => "", - :terms_and_conditions => "1", - }, - } - end - let(:success_response_status) { 201 } - let(:success_record_change_count) { 1 } - - it_behaves_like "admin token access", :post, :create - it_behaves_like "api key role access", :post, :create - it_behaves_like "admin role permissions", :post, :create - it_behaves_like "server-side timestamps", :post, :create - - it "performs an create" do - admin_token_auth(@admin) - expect do - post :create, params - - data = MultiJson.load(response.body) - data["user"]["last_name"].should eql("Potato") - - user = ApiUser.find(data["user"]["id"]) - user.last_name.should eql("Potato") - end.to change { ApiUser.count }.by(1) - end - - it "returns a validation error if the user attributes aren't present" do - admin_token_auth(@admin) - expect do - post :create, :format => "json" - - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to_not change { ApiUser.count } - end - - it "returns a validation error if the user attributes are an unexpected object" do - admin_token_auth(@admin) - expect do - post :create, :format => "json", :user => "something" - - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to_not change { ApiUser.count } - end - - it "returns a wildcard CORS response" do - admin_token_auth(@admin) - post :create, params - response.headers["Access-Control-Allow-Origin"].should eql("*") - end - - it "allows admins to set private fields" do - p = params - p[:user][:roles] = ["admin"] - - admin_token_auth(@admin) - post :create, p - - response.status.should eql(success_response_status) - - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.roles.should eql(["admin"]) - end - - it "forbids non-admins from setting private fields" do - p = params - p[:user][:roles] = ["admin"] - - request.env["HTTP_X_API_ROLES"] = "api-umbrella-key-creator" - post :create, p - - response.status.should eql(success_response_status) - - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.roles.should eql(nil) - end - - it "defaults the registration source to 'api'" do - admin_token_auth(@admin) - post :create, params - data = MultiJson.load(response.body) - data["user"]["registration_source"].should eql("api") - end - - it "allows setting a custom registration source" do - admin_token_auth(@admin) - p = params - p[:user][:registration_source] = "whatever" - post :create, p - data = MultiJson.load(response.body) - data["user"]["registration_source"].should eql("whatever") - end - - it "captures the requester's IP - does not trust x-forwarded-for from arbitrary IP" do - admin_token_auth(@admin) - request.env["REMOTE_ADDR"] = "48.146.218.185" - request.env["HTTP_X_FORWARDED_FOR"] = "3.3.3.3" - post :create, params - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.registration_ip.should eql("48.146.218.185") - end - - it "captures the requester's IP - does trust x-forwarded-for from local IP" do - admin_token_auth(@admin) - request.env["REMOTE_ADDR"] = "127.0.0.1" - request.env["HTTP_X_FORWARDED_FOR"] = "3.3.3.3" - post :create, params - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.registration_ip.should eql("3.3.3.3") - end - - it "captures the requester's user agent" do - admin_token_auth(@admin) - request.env["HTTP_USER_AGENT"] = "curl" - post :create, params - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.registration_user_agent.should eql("curl") - end - - it "captures the requester's referer" do - admin_token_auth(@admin) - request.env["HTTP_REFERER"] = "http://example.com/foo" - post :create, params - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.registration_referer.should eql("http://example.com/foo") - end - - it "captures the requester's origin" do - admin_token_auth(@admin) - request.env["HTTP_ORIGIN"] = "http://example.com" - post :create, params - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - user.registration_origin.should eql("http://example.com") - end - - it "returns registration requester information for admin users" do - admin_token_auth(@admin) - request.env["REMOTE_ADDR"] = "1.2.3.4" - request.env["HTTP_USER_AGENT"] = "curl" - request.env["HTTP_REFERER"] = "http://example.com/foo" - request.env["HTTP_ORIGIN"] = "http://example.com" - post :create, params - data = MultiJson.load(response.body) - data["user"]["registration_ip"].should eql("1.2.3.4") - data["user"]["registration_user_agent"].should eql("curl") - data["user"]["registration_referer"].should eql("http://example.com/foo") - data["user"]["registration_origin"].should eql("http://example.com") - end - - it "omits registration requester information for non-admin users" do - request.env["REMOTE_ADDR"] = "1.2.3.4" - request.env["HTTP_USER_AGENT"] = "curl" - request.env["HTTP_REFERER"] = "http://example.com/foo" - request.env["HTTP_ORIGIN"] = "http://example.com" - request.env["HTTP_X_API_ROLES"] = "api-umbrella-key-creator" - post :create, params - data = MultiJson.load(response.body) - data["user"]["registration_ip"].should eql(nil) - data["user"]["registration_user_agent"].should eql(nil) - data["user"]["registration_referer"].should eql(nil) - data["user"]["registration_origin"].should eql(nil) - end - - describe "e-mail verification" do - it "returns the api key immediately and does not mark the user as e-mail verified by default" do - request.env["HTTP_X_API_ROLES"] = "api-umbrella-key-creator" - post :create, params - data = MultiJson.load(response.body) - data["user"]["api_key"].should be_kind_of(String) - user = ApiUser.find(data["user"]["id"]) - user.email_verified.should eql(false) - end - - it "does not return the api key and marks the user as e-mail verified when requested" do - request.env["HTTP_X_API_ROLES"] = "api-umbrella-key-creator" - p = params - p[:options] = { :verify_email => true } - post :create, p - data = MultiJson.load(response.body) - data["user"]["api_key"].should eql(nil) - user = ApiUser.find(data["user"]["id"]) - user.email_verified.should eql(true) - end - - it "always marks the user as e-mail verified when an admin creates the account" do - admin_token_auth(@admin) - post :create, params - data = MultiJson.load(response.body) - data["user"]["api_key"].should be_kind_of(String) - user = ApiUser.find(data["user"]["id"]) - user.email_verified.should eql(true) - end - end - - describe "custom rate limits" do - it "cannot create limits of the same duration and limit_by" do - admin_token_auth(@admin) - - expect do - attributes = FactoryGirl.attributes_for(:api_user, { - :settings => FactoryGirl.attributes_for(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit_by => "ip", :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit_by => "ip", :limit => 20), - ], - }), - }) - - put :create, :format => "json", :user => attributes - - response.status.should eql(422) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to_not change { ApiUser.count } - end - - it "can create limits of the same duration with different limit_by" do - admin_token_auth(@admin) - - expect do - attributes = FactoryGirl.attributes_for(:api_user, { - :settings => FactoryGirl.attributes_for(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit_by => "ip", :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit_by => "apiKey", :limit => 20), - ], - }), - }) - - put :create, :format => "json", :user => attributes - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["user"]["settings"]["rate_limits"].length.should eql(2) - - user = ApiUser.find(data["user"]["id"]) - user.settings.rate_limits.length.should eql(2) - end.to change { ApiUser.count }.by(1) - end - end - - describe "welcome e-mail" do - before(:each) do - Delayed::Worker.delay_jobs = false - ActionMailer::Base.deliveries.clear - end - - after(:each) do - Delayed::Worker.delay_jobs = true - end - - it "sends a welcome e-mail to be sent when requested" do - admin_token_auth(@admin) - expect do - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "does not send welcome e-mails when explicitly disabled" do - admin_token_auth(@admin) - expect do - p = params - p[:options] = { :send_welcome_email => false } - post :create, p - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "does not send a welcome e-mail when the option is an unknown value" do - admin_token_auth(@admin) - expect do - p = params - p[:options] = { :send_welcome_email => 1 } - post :create, p - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "does not send welcome e-mails by default" do - admin_token_auth(@admin) - expect do - post :create, params - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "sends welcome e-mails when the user-based 'send_welcome_email' attribute is set to anything (for the admin tool/backwards compatibility)" do - admin_token_auth(@admin) - expect do - p = params - p[:user][:send_welcome_email] = "0" - post :create, p - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "queues a welcome e-mail to when delayed job is enabled" do - Delayed::Worker.delay_jobs = true - admin_token_auth(@admin) - expect do - expect do - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - end.to change { Delayed::Job.count }.by(1) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "sends the e-mail to the user that signed up" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - ActionMailer::Base.deliveries.first.to.should eql(["potato@example.com"]) - end - - it "includes the API key in the signup message" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - - ActionMailer::Base.deliveries.first.encoded.should include(user.api_key) - end - - describe "e-mail subject" do - it "defaults to the configured site name" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - ActionMailer::Base.deliveries.first.subject.should eql("Your API Umbrella API key") - end - - it "changes the e-mail subject based on the site name" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :site_name => "External Example" } - post :create, p - ActionMailer::Base.deliveries.first.subject.should eql("Your External Example API key") - end - end - - describe "from" do - it "defaults to using the configured host" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - ActionMailer::Base.deliveries.first.from.should eql(["noreply@localhost"]) - ActionMailer::Base.deliveries.first[:from].value.should eql("noreply@localhost") - end - - it "allows changing the from e-mail name" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :email_from_name => "Tester" } - post :create, p - ActionMailer::Base.deliveries.first.from.should eql(["noreply@localhost"]) - ActionMailer::Base.deliveries.first[:from].value.should eql("Tester ") - end - - it "allows changing the from e-mail address" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :email_from_address => "test@google.com" } - post :create, p - ActionMailer::Base.deliveries.first.from.should eql(["test@google.com"]) - ActionMailer::Base.deliveries.first[:from].value.should eql("test@google.com") - end - - it "allows changing the both the from e-mail address and name" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :email_from_name => "Tester", :email_from_address => "test@google.com" } - post :create, p - ActionMailer::Base.deliveries.first.from.should eql(["test@google.com"]) - ActionMailer::Base.deliveries.first[:from].value.should eql("Tester ") - end - end - - describe "example API url" do - it "defaults to no example URL" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - ActionMailer::Base.deliveries.first.encoded.should_not include("Here's an example") - end - - it "includes an example API url when given" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :example_api_url => "https://example.com/api.json?api_key={{api_key}}&test=1" } - post :create, p - ActionMailer::Base.deliveries.first.encoded.should include("Here's an example") - end - - it "formats the example URL by substituting the api key" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :example_api_url => "https://example.com/api.json?api_key={{api_key}}&test=1" } - post :create, p - - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - - ActionMailer::Base.deliveries.first.encoded.should include(%(https://example.com/api.json?api_key=#{user.api_key}&test=1)) - end - - it "offers a plain text version" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :example_api_url => "https://example.com/api.json?api_key={{api_key}}" } - post :create, p - - data = MultiJson.load(response.body) - user = ApiUser.find(data["user"]["id"]) - - ActionMailer::Base.deliveries.first.encoded.should include("https://example.com/api.json?api_key=#{user.api_key}\r\n( https://example.com/api.json?api_key=#{user.api_key} )") - end - end - - describe "contact URL" do - it "defaults to no example URL" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - ActionMailer::Base.deliveries.first.encoded.should include(%(contact us)) - end - - it "includes an example API url when given" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true, :contact_url => "https://example.com/contact-us" } - post :create, p - ActionMailer::Base.deliveries.first.encoded.should include(%(contact us)) - end - - it "offers a plain text version" do - admin_token_auth(@admin) - p = params - p[:options] = { :send_welcome_email => true } - post :create, p - ActionMailer::Base.deliveries.first.encoded.should include("contact us ( http://localhost/contact/ )") - end - end - end - - describe "notify e-mail" do - before(:each) do - Delayed::Worker.delay_jobs = false - ActionMailer::Base.deliveries.clear - ApiUmbrellaConfig[:web][:contact_form_email] = "aa@bb.com" - end - - after(:each) do - Delayed::Worker.delay_jobs = true - end - - it "sends a notify e-mail to be sent when requested in query" do - admin_token_auth(@admin) - expect do - p = params - p[:options] = { :send_notify_email => true } - post :create, p - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "sends a notify e-mail to be sent when requested in the config" do - admin_token_auth(@admin) - expect do - p = params - ApiUmbrellaConfig[:web][:send_notify_email] = true - post :create, p - ApiUmbrellaConfig[:web][:send_notify_email] = false - - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "does not send notify e-mails when explicitly disabled" do - admin_token_auth(@admin) - expect do - p = params - p[:options] = { :send_notify_email => false } - post :create, p - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "does not send a notify e-mail when the option is an unknown value" do - admin_token_auth(@admin) - expect do - p = params - p[:options] = { :send_notify_email => 1 } - post :create, p - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "does not send notify e-mails by default" do - admin_token_auth(@admin) - expect do - post :create, params - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "queues a welcome e-mail to when delayed job is enabled" do - Delayed::Worker.delay_jobs = true - admin_token_auth(@admin) - expect do - expect do - p = params - p[:options] = { :send_notify_email => true } - post :create, p - end.to change { Delayed::Job.count }.by(1) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - end - end - - describe "PUT update" do - before(:each) do - @api_user = FactoryGirl.create(:api_user) - end - - let(:params) do - { - :format => "json", - :id => @api_user.id, - :user => { - :first_name => "Bob", - }, - } - end - let(:success_response_status) { 200 } - let(:success_record_change_count) { 0 } - - it_behaves_like "admin token access", :put, :update - it_behaves_like "no api key role access", :put, :update - it_behaves_like "admin role permissions", :put, :update - it_behaves_like "server-side timestamps", :put, :update - - it "performs an update" do - admin_token_auth(@admin) - put :update, params - data = MultiJson.load(response.body) - data["user"]["first_name"].should eql("Bob") - - user = ApiUser.find(@api_user.id) - user.first_name.should eql("Bob") - end - - it "leaves existing registration sources alone" do - user = FactoryGirl.create(:api_user, :registration_source => "something") - admin_token_auth(@admin) - put :update, params.merge(:id => user.id) - data = MultiJson.load(response.body) - data["user"]["registration_source"].should eql("something") - end - - describe "allowed ips" do - it "adds allowed ips" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user) - user.settings.should eql(nil) - - attributes = user.as_json - attributes["settings"] ||= {} - attributes["settings"]["allowed_ips"] = ["127.0.0.1", "127.0.0.2"] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_ips.should eql(["127.0.0.1", "127.0.0.2"]) - end - - it "updates allowed ips" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:api_setting, { - :allowed_ips => ["127.0.0.1"], - }), - }) - user.settings.allowed_ips.should eql(["127.0.0.1"]) - - attributes = user.as_json - attributes["settings"]["allowed_ips"] = ["127.0.0.5", "127.0.0.4"] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_ips.should eql(["127.0.0.5", "127.0.0.4"]) - end - - it "removes allowed ips when set to empty array" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:api_setting, { - :allowed_ips => ["127.0.0.1"], - }), - }) - user.settings.allowed_ips.should eql(["127.0.0.1"]) - - attributes = user.as_json - attributes["settings"]["allowed_ips"] = [] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_ips.should eql([]) - end - - it "removes allowed ips when set to null" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:api_setting, { - :allowed_ips => ["127.0.0.1"], - }), - }) - user.settings.allowed_ips.should eql(["127.0.0.1"]) - - attributes = user.as_json - attributes["settings"]["allowed_ips"] = nil - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_ips.should eql(nil) - end - end - - describe "allowed referers" do - it "adds allowed referers" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user) - user.settings.should eql(nil) - - attributes = user.as_json - attributes["settings"] ||= {} - attributes["settings"]["allowed_referers"] = ["http://google.com/", "http://yahoo.com/"] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_referers.should eql(["http://google.com/", "http://yahoo.com/"]) - end - - it "updates allowed referers" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:api_setting, { - :allowed_referers => ["http://google.com/"], - }), - }) - user.settings.allowed_referers.should eql(["http://google.com/"]) - - attributes = user.as_json - attributes["settings"]["allowed_referers"] = ["https://example.com", "https://bing.com/foo"] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_referers.should eql(["https://example.com", "https://bing.com/foo"]) - end - - it "removes allowed referers when set to empty array" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:api_setting, { - :allowed_referers => ["http://google.com"], - }), - }) - user.settings.allowed_referers.should eql(["http://google.com"]) - - attributes = user.as_json - attributes["settings"]["allowed_referers"] = [] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_referers.should eql([]) - end - - it "removes allowed referers when set to null" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:api_setting, { - :allowed_referers => ["http://google.com"], - }), - }) - user.settings.allowed_referers.should eql(["http://google.com"]) - - attributes = user.as_json - attributes["settings"]["allowed_referers"] = nil - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.allowed_referers.should eql(nil) - end - - end - - describe "custom rate limits" do - it "updates embedded custom rate limit records" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), - ], - }), - }) - - attributes = user.as_json - attributes["settings"]["rate_limits"][0]["limit"] = 50 - attributes["settings"]["rate_limits"][1]["limit"] = 75 - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.rate_limits.length.should eql(2) - user.settings.rate_limits[0].duration.should eql(5000) - user.settings.rate_limits[0].limit.should eql(50) - user.settings.rate_limits[1].duration.should eql(10000) - user.settings.rate_limits[1].limit.should eql(75) - end - - it "removes embedded custom rate limit records" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), - ], - }), - }) - user.settings.rate_limits.length.should eql(2) - - attributes = user.as_json - attributes["settings"]["rate_limits"] = [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 1000, :limit => 5), - ] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.rate_limits.length.should eql(1) - user.settings.rate_limits[0].duration.should eql(1000) - user.settings.rate_limits[0].limit.should eql(5) - end - - it "clears embedded custom rate limit records when set to an empty array" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), - ], - }), - }) - user.settings.rate_limits.length.should eql(2) - - attributes = user.as_json - attributes["settings"]["rate_limits"] = [] - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.rate_limits.length.should eql(0) - end - - it "clears embedded custom rate limit records when set to null" do - admin_token_auth(@admin) - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), - ], - }), - }) - user.settings.rate_limits.length.should eql(2) - - attributes = user.as_json - attributes["settings"]["rate_limits"] = nil - - put :update, :format => "json", :id => user.id, :user => attributes - - user.reload - user.settings.rate_limits.length.should eql(0) - end - end - end - - describe "permissions" do - before(:each) do - DatabaseCleaner.clean - end - - describe "role permissions" do - it "prevents limited admins from updating forbidden users to only contain roles the admin does have permissions to" do - FactoryGirl.create(:google_api) - FactoryGirl.create(:yahoo_api) - existing_roles = ApiUserRole.all - existing_roles.should eql(["google-write", "yahoo-write"]) - - record = FactoryGirl.create(:api_user, { - :roles => ["yahoo-write"], - }) - - admin = FactoryGirl.create(:google_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["roles"] = ["google-write"] - put :update, :format => "json", :id => record.id, :user => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = ApiUser.find(record.id) - record.roles.should eql(["yahoo-write"]) - end - end - end -end diff --git a/src/api-umbrella/web-app/spec/controllers/api/v1/website_backends_controller_spec.rb b/src/api-umbrella/web-app/spec/controllers/api/v1/website_backends_controller_spec.rb deleted file mode 100644 index 96edd4eff..000000000 --- a/src/api-umbrella/web-app/spec/controllers/api/v1/website_backends_controller_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -require "spec_helper" - -describe Api::V1::WebsiteBackendsController do - before(:each) do - DatabaseCleaner.clean - end - - describe "admin permissions" do - shared_examples "admin permitted" do - describe "GET index" do - it "includes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should include(record.id) - end - end - - describe "GET show" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(200) - data = MultiJson.load(response.body) - data.keys.should eql(["website_backend"]) - end - end - - describe "POST create" do - it "permits access" do - attributes = FactoryGirl.attributes_for(@factory).stringify_keys - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :website_backend => attributes - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["website_backend"]["server_host"].should_not eql(nil) - data["website_backend"]["server_host"].should eql(attributes["server_host"]) - end.to change { WebsiteBackend.count }.by(1) - end - end - - describe "PUT update" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["server_host"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :website_backend => attributes - - response.status.should eql(204) - record = WebsiteBackend.find(record.id) - record.server_host.should_not eql(nil) - record.server_host.should eql(attributes["server_host"]) - end - end - - describe "DELETE destroy" do - it "permits access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - response.status.should eql(204) - end.to change { WebsiteBackend.count }.by(-1) - end - end - end - - shared_examples "admin forbidden" do - describe "GET index" do - it "excludes the group in the results" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :index, :format => "json" - - response.status.should eql(200) - data = MultiJson.load(response.body) - record_ids = data["data"].map { |r| r["id"] } - record_ids.should_not include(record.id) - end - end - - describe "GET show" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - get :show, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end - end - - describe "POST create" do - it "forbids access" do - attributes = FactoryGirl.attributes_for(@factory).stringify_keys - admin_token_auth(@admin) - - expect do - post :create, :format => "json", :website_backend => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { WebsiteBackend.count }.by(0) - end - end - - describe "PUT update" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - attributes = record.serializable_hash - attributes["server_host"] += rand(999_999).to_s - put :update, :format => "json", :id => record.id, :website_backend => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = WebsiteBackend.find(record.id) - record.server_host.should_not eql(nil) - record.server_host.should_not eql(attributes["server_host"]) - end - end - - describe "DELETE destroy" do - it "forbids access" do - record = FactoryGirl.create(@factory) - admin_token_auth(@admin) - - expect do - delete :destroy, :format => "json", :id => record.id - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - end.to change { WebsiteBackend.count }.by(0) - end - end - end - - describe "localhost website backend" do - before(:each) do - @factory = :website_backend - end - - it_behaves_like "admin permissions", :required_permissions => ["backend_manage"], :root_required => true - end - - it "prevents limited admins from updating forbidden website backends to use a host the admin does have permissions to" do - record = FactoryGirl.create(:website_backend, :frontend_host => "example.com") - - admin = FactoryGirl.create(:localhost_root_admin) - admin_token_auth(admin) - - attributes = record.serializable_hash - attributes["frontend_host"] = "localhost" - put :update, :format => "json", :id => record.id, :website_backend => attributes - - response.status.should eql(403) - data = MultiJson.load(response.body) - data.keys.should eql(["errors"]) - - record = WebsiteBackend.find(record.id) - record.frontend_host.should eql("example.com") - end - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_headers.rb b/src/api-umbrella/web-app/spec/factories/api_headers.rb deleted file mode 100644 index 3b3dc08ed..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_headers.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_header, :class => 'Api::Header' do - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_rate_limits.rb b/src/api-umbrella/web-app/spec/factories/api_rate_limits.rb deleted file mode 100644 index f6e6ab2f4..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_rate_limits.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_rate_limit, :class => 'Api::RateLimit' do - duration 60000 - limit_by "ip" - limit 500 - response_headers false - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_rewrites.rb b/src/api-umbrella/web-app/spec/factories/api_rewrites.rb deleted file mode 100644 index 23da2bc80..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_rewrites.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_rewrite, :class => 'Api::Rewrite' do - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_routes.rb b/src/api-umbrella/web-app/spec/factories/api_routes.rb deleted file mode 100644 index fb5db84ad..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_routes.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_route, :class => 'Api::Route' do - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_servers.rb b/src/api-umbrella/web-app/spec/factories/api_servers.rb deleted file mode 100644 index 7fbd76742..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_servers.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_server, :class => 'Api::Server' do - host "example.com" - port 80 - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_settings.rb b/src/api-umbrella/web-app/spec/factories/api_settings.rb deleted file mode 100644 index 4fca9b89a..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_settings.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_setting, :class => 'Api::Settings' do - factory :custom_rate_limit_api_setting do - rate_limit_mode "custom" - rate_limits do - [ - FactoryGirl.attributes_for(:api_rate_limit, :response_headers => true), - ] - end - end - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_sub_settings.rb b/src/api-umbrella/web-app/spec/factories/api_sub_settings.rb deleted file mode 100644 index 382eeb2fc..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_sub_settings.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_sub_setting, :class => 'Api::SubSettings' do - http_method "POST" - regex ".*" - end -end diff --git a/src/api-umbrella/web-app/spec/factories/api_url_matches.rb b/src/api-umbrella/web-app/spec/factories/api_url_matches.rb deleted file mode 100644 index 42e537356..000000000 --- a/src/api-umbrella/web-app/spec/factories/api_url_matches.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_url_match, :class => 'Api::UrlMatch' do - frontend_prefix "/example-frontend/" - backend_prefix "/example-backend/" - end -end diff --git a/src/api-umbrella/web-app/spec/factories/config_versions.rb b/src/api-umbrella/web-app/spec/factories/config_versions.rb deleted file mode 100644 index 6c33551f7..000000000 --- a/src/api-umbrella/web-app/spec/factories/config_versions.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :config_version do - end -end diff --git a/src/api-umbrella/web-app/spec/factories/log_items.rb b/src/api-umbrella/web-app/spec/factories/log_items.rb deleted file mode 100644 index 223e30862..000000000 --- a/src/api-umbrella/web-app/spec/factories/log_items.rb +++ /dev/null @@ -1,104 +0,0 @@ -require "elasticsearch/persistence/model" - -Elasticsearch::Persistence.client = Elasticsearch::Client.new({ - :hosts => ApiUmbrellaConfig[:elasticsearch][:hosts], - :logger => Rails.logger -}) - -class LogItem - include Elasticsearch::Persistence::Model - - index_name "api-umbrella-logs-write-2015-01" - document_type "log" - - attribute :api_key, String - attribute :backend_response_time, Integer - attribute :gatekeeper_denied_code, String - attribute :internal_gatekeeper_time, Float - attribute :internal_response_time, Float - attribute :proxy_overhead, Integer - attribute :request_accept_encoding, String - attribute :request_at, Time - attribute :request_hierarchy, Array - attribute :request_host, String - attribute :request_ip, String - attribute :request_ip_city, String - attribute :request_ip_country, String - attribute :request_ip_region, String - attribute :request_method, String - attribute :request_path, String - attribute :request_query, Hash - attribute :request_scheme, String - attribute :request_size, Integer - attribute :request_url, String - attribute :request_user_agent, String - attribute :request_user_agent_family, String - attribute :request_user_agent_type, String - attribute :response_age, Integer - attribute :response_content_length, Integer - attribute :response_content_type, String - attribute :response_server, String - attribute :response_size, Integer - attribute :response_status, Integer - attribute :response_time, Integer - attribute :user_email, String - attribute :user_id, String - attribute :user_registration_source, String - - def save! - self.save || raise("Failed to save log") - end -end - -FactoryGirl.define do - factory :log_item do - api_key "UfhkQUBgWQbJ0ZVqnJ4TvA7quGCZHYTFCXwSfOTQ" - backend_response_time 0 - internal_gatekeeper_time 1.4 - internal_response_time 1.8 - proxy_overhead 3 - request_accept_encoding "*/*" - request_at { Time.now } - request_hierarchy ["0/127.0.0.1/", "1/127.0.0.1/hello"] - request_host "127.0.0.1" - request_ip "127.0.0.1" - request_ip_city "Golden" - request_ip_country "US" - request_ip_region "CO" - request_method "GET" - request_path "/hello/" - request_query({ "foo" => "bar" }) - request_scheme "http" - request_size 140 - request_url "http://127.0.0.1/hello/?foo=bar" - request_user_agent "ApacheBench/2.3" - request_user_agent_family "AB (Apache Bench)" - request_user_agent_type "Other" - response_age 0 - response_content_length 14 - response_content_type "text/plain" - response_server "nginx" - response_size 243 - response_status 200 - response_time 3 - user_email "test@example.com" - user_id "4199b260-ae76-463f-8395-d30de09c1540" - user_registration_source "web_admin" - - factory :xss_log_item do - request_accept_encoding '">' - request_host '">' - request_ip_city '">' - request_ip_country '">' - request_ip_region '">' - request_path '">' - request_query({ "foo" => '">' }) - request_url '">' - request_user_agent '">' - response_content_type '">' - response_server '">' - user_email '">' - user_registration_source '">' - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/admins_spec.rb b/src/api-umbrella/web-app/spec/features/admin/admins_spec.rb deleted file mode 100644 index 2423c98c7..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/admins_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'spec_helper' - -describe "admins form", :js => true do - describe "superuser is logged in" do - login_admin - - it "shows the superuser checkbox" do - visit "/admin/#/admins/new" - - page.should have_content("Username") - page.should have_content("Superuser") - end - end - - describe "limited admin is logged in" do - let(:current_admin) { FactoryGirl.create(:limited_admin) } - login_admin - - it "hides the superuser checkbox" do - visit "/admin/#/admins/new" - - page.should have_content("Username") - page.should_not have_content("Superuser") - end - end - - describe "group checkboxes" do - login_admin - - before(:all) do - @group1 = FactoryGirl.create(:admin_group) - @group2 = FactoryGirl.create(:admin_group) - @group3 = FactoryGirl.create(:admin_group) - end - - it "adds groups to the admin account when checkboxes are checked" do - admin = FactoryGirl.create(:admin) - admin.group_ids.should eql([]) - - visit "/admin/#/admins/#{admin.id}/edit" - - check @group1.name - check @group3.name - - click_button("Save") - - page.should have_content("Successfully saved the admin") - - admin = Admin.find(admin.id) - admin.group_ids.sort.should eql([@group1.id, @group3.id].sort) - end - - it "removes groups from the admin account when checkboxes are unchecked" do - admin = FactoryGirl.create(:admin, :groups => [@group1, @group2]) - admin.group_ids.sort.should eql([@group1.id, @group2.id].sort) - - visit "/admin/#/admins/#{admin.id}/edit" - - uncheck @group1.name - uncheck @group2.name - check @group3.name - - click_button("Save") - - page.should have_content("Successfully saved the admin") - - admin = Admin.find(admin.id) - admin.group_ids.sort.should eql([@group3.id].sort) - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/api_users_spec.rb b/src/api-umbrella/web-app/spec/features/admin/api_users_spec.rb deleted file mode 100644 index 916fff336..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/api_users_spec.rb +++ /dev/null @@ -1,345 +0,0 @@ -require 'spec_helper' - -describe "api users form", :js => true do - login_admin - - describe "xss" do - before(:all) do - ApiUser.where(:registration_source.ne => "seed").delete_all - @user = FactoryGirl.create(:xss_api_user) - end - - it "escapes html entities in the table" do - visit "/admin/#/api_users" - - page.should have_content(@user.email) - page.should have_content(@user.first_name) - page.should have_content(@user.last_name) - page.should have_content(@user.use_description) - page.should have_content(@user.registration_source) - page.should_not have_selector(".xss-test", :visible => :all) - end - - it "escapes html entities in the form" do - visit "/admin/#/api_users/#{@user.id}/edit" - - find_field("E-mail").value.should eql(@user.email) - find_field("First Name").value.should eql(@user.first_name) - find_field("Last Name").value.should eql(@user.last_name) - find_field("Purpose").value.should eql(@user.use_description) - page.should have_content(@user.registration_source) - page.should_not have_selector(".xss-test", :visible => :all) - end - - it "escapes html entities in flash confirmation message" do - visit "/admin/#/api_users/#{@user.id}/edit" - - fill_in "Last Name", :with => "Doe" - click_button("Save") - - page.should have_content("Successfully saved the user \"#{@user.email}\"") - page.should_not have_selector(".xss-test", :visible => :all) - end - end - - describe "api key visibility" do - it "shows the api key in the save notification when creating a new account" do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - click_button("Save") - - page.should have_content("Successfully saved the user") - user = ApiUser.order_by(:created_at.asc).last - user.last_name.should eql("Doe") - page.should have_content(user.api_key) - end - - it "allows the full api key to be revealed when the admin has permissions" do - user = FactoryGirl.create(:api_user, :created_by => @current_admin.id, :created_at => Time.now - 2.weeks + 5.minutes) - visit "/admin/#/api_users/#{user.id}/edit" - - page.should have_content(user.api_key_preview) - page.should_not have_content(user.api_key) - page.should have_link("(reveal)") - click_link("(reveal)") - page.should have_content(user.api_key) - page.should_not have_content(user.api_key_preview) - page.should_not have_link("(reveal)") - page.should have_link("(hide)") - click_link("(hide)") - page.should have_content(user.api_key_preview) - page.should_not have_content(user.api_key) - page.should have_link("(reveal)") - end - - describe "limited admin is logged in" do - let(:current_admin) { FactoryGirl.create(:limited_admin) } - login_admin - - it "hides the full api key when the admin does not have permissions" do - user = FactoryGirl.create(:api_user, :created_by => @current_admin.id, :created_at => (Time.now - 2.weeks - 5.minutes)) - visit "/admin/#/api_users/#{user.id}/edit" - - page.should have_content(user.api_key_preview) - page.should_not have_content(user.api_key) - page.should_not have_link("(reveal)") - end - end - end - - describe "allowed ips input" do - it "saves an empty input as nil" do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - click_button("Save") - - page.should have_content("Successfully saved the user") - user = ApiUser.order_by(:created_at.asc).last - user.settings.allowed_ips.should eql(nil) - end - - it "saves multiple lines (omitting blank lines) as an array" do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - fill_in "Restrict Access to IPs", :with => "10.0.0.0/8\n\n\n\n127.0.0.1" - click_button("Save") - - page.should have_content("Successfully saved the user") - user = ApiUser.order_by(:created_at.asc).last - user.settings.allowed_ips.should eql(["10.0.0.0/8", "127.0.0.1"]) - end - - it "displays an existing array as multiple lines" do - user = FactoryGirl.create(:api_user, :settings => { :allowed_ips => ["10.0.0.0/24", "10.2.2.2"] }) - visit "/admin/#/api_users/#{user.id}/edit" - - find_field("Restrict Access to IPs").value.should eql("10.0.0.0/24\n10.2.2.2") - end - - it "nullifies an existing array when an empty input is saved" do - user = FactoryGirl.create(:api_user, :settings => { :allowed_ips => ["10.0.0.0/24", "10.2.2.2"] }) - visit "/admin/#/api_users/#{user.id}/edit" - - find_field("Restrict Access to IPs").value.should eql("10.0.0.0/24\n10.2.2.2") - fill_in "Restrict Access to IPs", :with => "" - click_button("Save") - - page.should have_content("Successfully saved the user") - user.reload - user.settings.allowed_ips.should eql(nil) - end - end - - describe "allowed referers input" do - it "saves an empty input as nil" do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - click_button("Save") - - page.should have_content("Successfully saved the user") - user = ApiUser.order_by(:created_at.asc).last - user.settings.allowed_referers.should eql(nil) - end - - it "saves multiple lines (omitting blank lines) as an array" do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - fill_in "Restrict Access to HTTP Referers", :with => "*.example.com/*\n\n\n\nhttp://google.com/*" - click_button("Save") - - page.should have_content("Successfully saved the user") - user = ApiUser.order_by(:created_at.asc).last - user.settings.allowed_referers.should eql(["*.example.com/*", "http://google.com/*"]) - end - - it "displays an existing array as multiple lines" do - user = FactoryGirl.create(:api_user, :settings => { :allowed_referers => ["*.example.com/*", "http://google.com/*"] }) - visit "/admin/#/api_users/#{user.id}/edit" - - find_field("Restrict Access to HTTP Referers").value.should eql("*.example.com/*\nhttp://google.com/*") - end - - it "nullifies an existing array when an empty input is saved" do - user = FactoryGirl.create(:api_user, :settings => { :allowed_referers => ["*.example.com/*", "http://google.com/*"] }) - visit "/admin/#/api_users/#{user.id}/edit" - - find_field("Restrict Access to HTTP Referers").value.should eql("*.example.com/*\nhttp://google.com/*") - fill_in "Restrict Access to HTTP Referers", :with => "" - click_button("Save") - - page.should have_content("Successfully saved the user") - user.reload - user.settings.allowed_referers.should eql(nil) - end - end - - describe "welcome e-mail" do - before(:each) do - Delayed::Worker.delay_jobs = false - ActionMailer::Base.deliveries.clear - end - - after(:each) do - Delayed::Worker.delay_jobs = true - end - - it "defaults to not sending it when signing up via the admin" do - expect do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - click_button("Save") - page.should have_content("Successfully saved the user") - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "sends the e-mail when explicitly asked for" do - expect do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - check "Send user welcome e-mail with API key information" - click_button("Save") - page.should have_content("Successfully saved the user") - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - end - - it "fills out and saves all the expected fields" do - visit "/admin/#/api_users/new" - - # User Info - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - - # Rate Limiting - select "Custom rate limits", :from => "Rate Limit" - find("button", :text => /Add Rate Limit/).click - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").set("2") - find(".rate-limit-duration-units").select("hours") - find(".rate-limit-limit-by").select("IP Address") - find(".rate-limit-limit").set("1500") - find(".rate-limit-response-headers").click - end - select "Rate limit by IP address", :from => "Limit By" - - # Permissions - fill_in "Roles", :with => "some-user-role" - find(".selectize-dropdown-content div", :text => /Add some-user-role/).click - find("body").native.send_key(:Escape) # Sporadically seems necessary to reset selectize properly for second input. - fill_in "Roles", :with => "some-user-role2" - find(".selectize-dropdown-content div", :text => /Add some-user-role2/).click - fill_in "Restrict Access to IPs", :with => "127.0.0.1\n10.1.1.1/16" - fill_in "Restrict Access to HTTP Referers", :with => "*.example.com/*\n*//example2.com/*" - select "Disabled", :from => "Account Enabled" - - click_button("Save") - page.should have_content("Successfully saved") - - user = ApiUser.desc(:created_at).first - visit "/admin/#/api_users/#{user.id}/edit" - - # User Info - page.should have_field("E-mail", :with => "example@example.com") - page.should have_field("First Name", :with => "John") - page.should have_field("Last Name", :with => "Doe") - - # Rate Limiting - page.should have_select("Rate Limit", :selected => "Custom rate limits") - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").value.should eql("2") - find(".rate-limit-duration-units").value.should eql("hours") - find(".rate-limit-limit-by").value.should eql("ip") - find(".rate-limit-limit").value.should eql("1500") - find(".rate-limit-response-headers").checked?.should eql(true) - end - page.should have_select("Limit By", :selected => "Rate limit by IP address") - - # Permissions - find_by_id(find_field("Roles")["data-raw-input-id"], :visible => :all).value.should eql("some-user-role,some-user-role2") - find_by_id(find_field("Roles")["data-selectize-control-id"]).text.should eql("some-user-role×some-user-role2×") - page.should have_field("Restrict Access to IPs", :with => "127.0.0.1\n10.1.1.1/16") - page.should have_field("Restrict Access to HTTP Referers", :with => "*.example.com/*\n*//example2.com/*") - page.should have_select("Account Enabled", :selected => "Disabled") - end - - it "edits custom rate limits" do - user = FactoryGirl.create(:custom_rate_limit_api_user) - visit "/admin/#/api_users/#{user.id}/edit" - - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").value.should eql("1") - find(".rate-limit-duration-units").value.should eql("minutes") - find(".rate-limit-limit-by").value.should eql("ip") - find(".rate-limit-limit").value.should eql("500") - find(".rate-limit-response-headers").checked?.should eql(true) - - find(".rate-limit-limit").set("200") - end - - click_button("Save") - page.should have_content("Successfully saved") - - user.reload - - user.settings.rate_limits.length.should eql(1) - rate_limit = user.settings.rate_limits.first - rate_limit.limit.should eql(200) - end - - it "removes custom rate limits" do - user = FactoryGirl.create(:api_user, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { - :rate_limits => [ - FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), - FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), - ], - }), - }) - - user.settings.rate_limits.length.should eql(2) - - visit "/admin/#/api_users/#{user.id}/edit" - - first(".custom-rate-limits-table a", :text => /Remove/).click - click_link("OK") - - click_button("Save") - page.should have_content("Successfully saved") - - user.reload - - user.settings.rate_limits.length.should eql(1) - rate_limit = user.settings.rate_limits.first - rate_limit.limit.should eql(20) - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/apis_spec.rb b/src/api-umbrella/web-app/spec/features/admin/apis_spec.rb deleted file mode 100644 index 4d321e855..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/apis_spec.rb +++ /dev/null @@ -1,444 +0,0 @@ -require 'spec_helper' - -describe "apis", :js => true do - login_admin - - before(:each) do - Api.delete_all - end - - describe "reordering" do - before(:each) do - FactoryGirl.create(:api, :name => "API A", :sort_order => 3) - FactoryGirl.create(:api, :name => "API B", :sort_order => 1) - FactoryGirl.create(:api, :name => "API C", :sort_order => 2) - FactoryGirl.create(:api, :name => "API testing-filter", :sort_order => 4) - end - - it "shows the drag handle when the reorder button is clicked and hides when the done button is clicked" do - visit "/admin/#/apis" - - all("tbody td.reorder-handle").length.should eql(0) - click_button "Reorder" - sleep 2 - all("tbody td.reorder-handle").length.should eql(4) - click_button "Done" - sleep 2 - all("tbody td.reorder-handle").length.should eql(0) - end - - it "removes filters when in reorder mode" do - visit "/admin/#/apis" - - all("tbody tr").length.should eql(4) - find(".dataTables_filter input").set("testing-fi") - wait_for_datatables_filter - all("tbody tr").length.should eql(1) - click_button "Reorder" - wait_for_datatables_filter - all("tbody tr").length.should eql(4) - end - - it "forces sorting by matching order when in reorder mode" do - visit "/admin/#/apis" - names = all("tbody td:first-child").map { |cell| cell.text } - names.should eql(["API A", "API B", "API C", "API testing-filter"]) - click_button "Reorder" - sleep 3 - names = all("tbody td:first-child").map { |cell| cell.text } - names.should eql(["API B", "API C", "API A", "API testing-filter"]) - end - - it "exits reorder mode when a filter is applied after entering reorder mode" do - visit "/admin/#/apis" - click_button "Reorder" - sleep 2 - all("tbody td.reorder-handle").length.should eql(4) - find(".dataTables_filter input").set("testing-fi") - wait_for_datatables_filter - sleep 2 - all("tbody td.reorder-handle").length.should eql(0) - end - - it "exits reorder mode when an order is applied after entering reorder mode" do - visit "/admin/#/apis" - click_button "Reorder" - sleep 2 - all("tbody td.reorder-handle").length.should eql(4) - find("thead tr:first-child").click - sleep 2 - all("tbody td.reorder-handle").length.should eql(0) - end - - it "performs reordering on drag" do - visit "/admin/#/apis" - - names = Api.sorted.all.map { |api| api.name } - names.should eql(["API B", "API C", "API A", "API testing-filter"]) - - click_button "Reorder" - - # Simulate the drag and drop using jquery-simulate-ext (capybara supports - # dropping, but not the dragging behavior jquery-ui needs). - page.execute_script %{ - $('tbody td:contains("API A")') - .siblings('td.reorder-handle') - .simulate('drag-n-drop', { dy: -70 }); - } - wait_for_ajax - - names = Api.sorted.all.map { |api| api.name } - names.should eql(["API A", "API B", "API C", "API testing-filter"]) - end - end - - describe "saving" do - before(:each) do - @api = FactoryGirl.create(:api_with_settings, :name => "Save Test API") - end - - it "saves the record when only the nested object attributes contain changes" do - @api.settings.error_data.should eql(nil) - - visit "/admin/#/apis" - click_link "Save Test API" - - find_field("Name").value.should eql("Save Test API") - - find("legend a", :text => /Advanced Settings/).click - page.execute_script %{ - ace.edit($('[data-form-property=api_key_missing]')[0]).setValue('hello1: foo\\nhello2: bar'); - } - - click_button("Save") - page.should have_content("Successfully saved") - - @api = Api.find(@api.id) - @api.settings.error_data.should eql({ - "api_key_missing" => { - "hello1" => "foo", - "hello2" => "bar", - } - }) - end - end - - describe "loading" do - before(:each) do - @api = FactoryGirl.create(:api_with_settings, :name => "Test Load API", :frontend_host => "example1.com") - end - - it "loads the record from the server each time the form opens, even if the data is pre-cached" do - visit "/admin/#/apis" - page.should have_content("Add API Backend") - - click_link "Test Load API" - find_field("Frontend Host").value.should eql("example1.com") - - find("nav a", :text => /Configuration/).click - find("nav a", :text => /API Backends/).click - page.should have_content("Add API Backend") - - @api.frontend_host = "example2.com" - @api.save! - - click_link "Test Load API" - find_field("Frontend Host").value.should eql("example2.com") - end - end - - it "returns a validation error when all servers are removed from an existing API" do - api = FactoryGirl.create(:api) - visit "/admin/#/apis/#{api.id}/edit" - find("#servers_table a", :text => /Remove/).click - click_link("OK") - click_button("Save") - page.should have_content("must have at least one servers") - - api = Api.find(api.id) - api.servers.length.should eql(1) - end - - it "returns a validation error when all url prefixes are removed from an existing API" do - api = FactoryGirl.create(:api) - visit "/admin/#/apis/#{api.id}/edit" - find("#url_matches_table a", :text => /Remove/).click - click_link("OK") - click_button("Save") - page.should have_content("must have at least one url_matches") - - api = Api.find(api.id) - api.url_matches.length.should eql(1) - end - - it "shows the roles override checkbox only in the sub-settings" do - visit "/admin/#/apis/new" - - find("legend a", :text => /Global Request Settings/).click - page.should_not have_field('Override required roles from "Global Request Settings"') - - find("legend a", :text => /Sub-URL Request Settings/).click - find("button", :text => /Add URL Settings/).click - within(".modal") do - page.should have_field('Override required roles from "Global Request Settings"') - end - end - - it "defaults the frontend host to the current url's hostname" do - visit "/admin/#/apis/new" - page.should have_field("Frontend Host", :with => "127.0.0.1") - end - - it "fills out and saves all the expected fields" do - visit "/admin/#/apis/new" - - fill_in "Name", :with => "Testing API Backend" - - # Backend - select "https", :from => "Backend Protocol" - find("button", :text => /Add Server/).click - within(".modal") do - fill_in "Host", :with => "google.com" - fill_in "Port", :with => "443" - click_button("OK") - end - - # Host - fill_in "Frontend Host", :with => "api.foo.com" - fill_in "Backend Host", :with => "api.bar.com" - - # Matching URL Prefixes - find("button", :text => /Add URL Prefix/).click - within(".modal") do - fill_in "Frontend prefix", :with => "/foo" - fill_in "Backend prefix", :with => "/bar" - click_button("OK") - end - - # Global Request Settings - find("legend a", :text => /Global Request Settings/).click - fill_in "Append Query String Parameters", :with => "foo=bar" - fill_in "Set Request Headers", :with => "X-Foo1: Bar\nX-Bar2: Foo" - fill_in "HTTP Basic Authentication", :with => "foo:bar" - select "Optional - HTTPS is optional", :from => "HTTPS Requirements" - select "Disabled - API keys are optional", :from => "API Key Checks" - select "None - API keys can be used without any verification", :from => "API Key Verification Requirements" - fill_in "Required Roles", :with => "some-role" - find(".selectize-dropdown-content div", :text => /Add some-role/).click - find("body").native.send_key(:Escape) # Sporadically seems necessary to reset selectize properly for second input. - fill_in "Required Roles", :with => "some-role2" - find(".selectize-dropdown-content div", :text => /Add some-role2/).click - check "Via HTTP header" - check "Via GET query parameter" - select "Custom rate limits", :from => "Rate Limit" - find("button", :text => /Add Rate Limit/).click - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").set("2") - find(".rate-limit-duration-units").select("hours") - find(".rate-limit-limit-by").select("IP Address") - find(".rate-limit-limit").set("1500") - find(".rate-limit-response-headers").click - end - select "IP Only - API key rate limits are ignored (only IP based limits are applied)", :from => "Anonymous Rate Limit Behavior" - select "API Key Only - IP based rate limits are ignored (only API key limits are applied)", :from => "Authenticated Rate Limit Behavior" - fill_in "Default Response Headers", :with => "X-Foo2: Bar\nX-Bar2: Foo" - fill_in "Override Response Headers", :with => "X-Foo3: Bar\nX-Bar3: Foo" - - # Sub-URL Request Settings - find("legend a", :text => /Sub-URL Request Settings/).click - find("button", :text => /Add URL Settings/).click - within(".modal") do - select "OPTIONS", :from => "Http method" - fill_in "Regex", :with => "^/foo.*" - select "Required & return message - HTTP requests will receive a message to use HTTPS", :from => "HTTPS Requirements" - select "Disabled - API keys are optional", :from => "API Key Checks" - select "E-mail verification required - Existing API keys will break, only new API keys will work if verified", :from => "API Key Verification Requirements" - fill_in "Required Roles", :with => "sub-role" - # Within this modal, selectize acts a bit funky in Capybara, so we have - # to use javascript to click the add div, rather than Capybara like we do - # in our other selectize tests. - page.execute_script("$('.selectize-dropdown-content div').mousedown()") - check 'Override required roles from "Global Request Settings"' - check "Via HTTP header" - check "Via GET query parameter" - select "Custom rate limits", :from => "Rate Limit" - find("button", :text => /Add Rate Limit/).click - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").set("3") - find(".rate-limit-duration-units").select("minutes") - find(".rate-limit-limit-by").select("IP Address") - find(".rate-limit-limit").set("100") - find(".rate-limit-response-headers").click - end - select "IP Only - API key rate limits are ignored (only IP based limits are applied)", :from => "Anonymous Rate Limit Behavior" - select "API Key Only - IP based rate limits are ignored (only API key limits are applied)", :from => "Authenticated Rate Limit Behavior" - fill_in "Default Response Headers", :with => "X-Sub-Foo2: Bar\nX-Sub-Bar2: Foo" - fill_in "Override Response Headers", :with => "X-Sub-Foo3: Bar\nX-Sub-Bar3: Foo" - click_button("OK") - end - - # Advanced Requests Rewriting - find("legend a", :text => /Advanced Requests Rewriting/).click - find("button", :text => /Add Rewrite/).click - within(".modal") do - select "Regular Expression", :from => "Matcher type" - select "PUT", :from => "Http method" - fill_in "Frontend matcher", :with => "[0-9]+" - fill_in "Backend replacement", :with => "number" - click_button("OK") - end - - # Advanced Settings - find("legend a", :text => /Advanced Settings/).click - fill_in "JSON Template", :with => '{"foo":"bar"}' - fill_in "XML Template", :with => "bar" - fill_in "CSV Template", :with => "foo,bar\nbar,foo" - fill_in "Common (All Errors)", :with => "foo0: bar0\nbar0: foo0" - fill_in "API Key Missing", :with => "foo1: bar1\nbar1: foo1" - fill_in "API Key Invalid", :with => "foo2: bar2\nbar2: foo2" - fill_in "API Key Disabled", :with => "foo3: bar3\nbar3: foo3" - fill_in "API Key Unauthorized", :with => "foo4: bar4\nbar4: foo4" - fill_in "Over Rate Limit", :with => "foo5: bar5\nbar5: foo5" - fill_in "HTTPS Required", :with => "foo6: bar6\nbar6: foo6" - - click_button("Save") - page.should have_content("Successfully saved") - - api = Api.desc(:created_at).first - visit "/admin/#/apis/#{api.id}/edit" - - page.should have_field("Name", :with => "Testing API Backend") - - # Backend - page.should have_select("Backend Protocol", :selected => "https") - find("#servers_table a", :text => /Edit/).click - within(".modal") do - page.should have_field("Host", :with => "google.com") - page.should have_field("Port", :with => "443") - click_button("OK") - end - - # Host - page.should have_field("Frontend Host", :with => "api.foo.com") - page.should have_field("Backend Host", :with => "api.bar.com") - - # Matching URL Prefixes - find("#url_matches_table a", :text => /Edit/).click - within(".modal") do - page.should have_field("Frontend prefix", :with => "/foo") - page.should have_field("Backend prefix", :with => "/bar") - click_button("OK") - end - - # Global Request Settings - find("legend a", :text => /Global Request Settings/).click - page.should have_field("Append Query String Parameters", :with => "foo=bar") - page.should have_field("Set Request Headers", :with => "X-Foo1: Bar\nX-Bar2: Foo") - page.should have_field("HTTP Basic Authentication", :with => "foo:bar") - page.should have_select("HTTPS Requirements", :selected => "Optional - HTTPS is optional") - page.should have_select("API Key Checks", :selected => "Disabled - API keys are optional") - page.should have_select("API Key Verification Requirements", :selected => "None - API keys can be used without any verification") - find_by_id(find_field("Required Roles")["data-raw-input-id"], :visible => :all).value.should eql("some-role,some-role2") - find_by_id(find_field("Required Roles")["data-selectize-control-id"]).text.should eql("some-role×some-role2×") - page.should have_checked_field("Via HTTP header") - page.should have_checked_field("Via GET query parameter") - page.should have_select("Rate Limit", :selected => "Custom rate limits") - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").value.should eql("2") - find(".rate-limit-duration-units").value.should eql("hours") - find(".rate-limit-limit-by").value.should eql("ip") - find(".rate-limit-limit").value.should eql("1500") - find(".rate-limit-response-headers").checked?.should eql(true) - end - page.should have_select("Anonymous Rate Limit Behavior", :selected => "IP Only - API key rate limits are ignored (only IP based limits are applied)") - page.should have_select("Authenticated Rate Limit Behavior", :selected => "API Key Only - IP based rate limits are ignored (only API key limits are applied)") - page.should have_field("Default Response Headers", :with => "X-Foo2: Bar\nX-Bar2: Foo") - page.should have_field("Override Response Headers", :with => "X-Foo3: Bar\nX-Bar3: Foo") - - # Sub-URL Request Settings - find("legend a", :text => /Sub-URL Request Settings/).click - find("#sub_settings_table a", :text => /Edit/).click - within(".modal") do - page.should have_select("Http method", :selected => "OPTIONS") - page.should have_field("Regex", :with => "^/foo.*") - page.should have_select("HTTPS Requirements", :selected => "Required & return message - HTTP requests will receive a message to use HTTPS") - page.should have_select("API Key Checks", :selected => "Disabled - API keys are optional") - page.should have_select("API Key Verification Requirements", :selected => "E-mail verification required - Existing API keys will break, only new API keys will work if verified") - find_by_id(find_field("Required Roles")["data-raw-input-id"], :visible => :all).value.should eql("sub-role") - find_by_id(find_field("Required Roles")["data-selectize-control-id"]).text.should eql("sub-role×") - page.should have_checked_field('Override required roles from "Global Request Settings"') - page.should have_checked_field("Via HTTP header") - page.should have_checked_field("Via GET query parameter") - page.should have_select("Rate Limit", :selected => "Custom rate limits") - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").value.should eql("3") - find(".rate-limit-duration-units").value.should eql("minutes") - find(".rate-limit-limit-by").value.should eql("ip") - find(".rate-limit-limit").value.should eql("100") - find(".rate-limit-response-headers").checked?.should eql(true) - end - page.should have_select("Anonymous Rate Limit Behavior", :selected => "IP Only - API key rate limits are ignored (only IP based limits are applied)") - page.should have_select("Authenticated Rate Limit Behavior", :selected => "API Key Only - IP based rate limits are ignored (only API key limits are applied)") - page.should have_field("Default Response Headers", :with => "X-Sub-Foo2: Bar\nX-Sub-Bar2: Foo") - page.should have_field("Override Response Headers", :with => "X-Sub-Foo3: Bar\nX-Sub-Bar3: Foo") - click_button("OK") - end - - # Advanced Requests Rewriting - find("legend a", :text => /Advanced Requests Rewriting/).click - find("#rewrites a", :text => /Edit/).click - within(".modal") do - page.should have_select("Matcher type", :selected => "Regular Expression") - page.should have_select("Http method", :selected => "PUT") - page.should have_field("Frontend matcher", :with => "[0-9]+") - page.should have_field("Backend replacement", :with => "number") - click_button("OK") - end - - # Advanced Settings - find("legend a", :text => /Advanced Settings/).click - find_by_id(find_field("JSON Template")["data-raw-input-id"], :visible => :all).value.should eql('{"foo":"bar"}') - find_by_id(find_field("JSON Template")["data-ace-content-id"]).text.should eql('{"foo":"bar"}') - find_by_id(find_field("XML Template")["data-raw-input-id"], :visible => :all).value.should eql("bar") - find_by_id(find_field("XML Template")["data-ace-content-id"]).text.should eql("bar") - find_by_id(find_field("CSV Template")["data-raw-input-id"], :visible => :all).value.should eql("foo,bar\nbar,foo") - find_by_id(find_field("CSV Template")["data-ace-content-id"]).text.should eql("foo,bar bar,foo") - find_by_id(find_field("API Key Missing")["data-raw-input-id"], :visible => :all).value.should eql("foo1: bar1\nbar1: foo1") - find_by_id(find_field("API Key Missing")["data-ace-content-id"]).text.should eql("foo1: bar1 bar1: foo1") - find_by_id(find_field("API Key Invalid")["data-raw-input-id"], :visible => :all).value.should eql("foo2: bar2\nbar2: foo2") - find_by_id(find_field("API Key Invalid")["data-ace-content-id"]).text.should eql("foo2: bar2 bar2: foo2") - find_by_id(find_field("API Key Disabled")["data-raw-input-id"], :visible => :all).value.should eql("foo3: bar3\nbar3: foo3") - find_by_id(find_field("API Key Disabled")["data-ace-content-id"]).text.should eql("foo3: bar3 bar3: foo3") - find_by_id(find_field("API Key Unauthorized")["data-raw-input-id"], :visible => :all).value.should eql("foo4: bar4\nbar4: foo4") - find_by_id(find_field("API Key Unauthorized")["data-ace-content-id"]).text.should eql("foo4: bar4 bar4: foo4") - find_by_id(find_field("Over Rate Limit")["data-raw-input-id"], :visible => :all).value.should eql("foo5: bar5\nbar5: foo5") - find_by_id(find_field("Over Rate Limit")["data-ace-content-id"]).text.should eql("foo5: bar5 bar5: foo5") - end - - it "edits custom rate limits" do - api = FactoryGirl.create(:api, { - :settings => FactoryGirl.build(:custom_rate_limit_api_setting), - }) - visit "/admin/#/apis/#{api.id}/edit" - - find("legend a", :text => /Global Request Settings/).click - within(".custom-rate-limits-table") do - find(".rate-limit-duration-in-units").value.should eql("1") - find(".rate-limit-duration-units").value.should eql("minutes") - find(".rate-limit-limit-by").value.should eql("ip") - find(".rate-limit-limit").value.should eql("500") - find(".rate-limit-response-headers").checked?.should eql(true) - - find(".rate-limit-limit").set("200") - end - - click_button("Save") - page.should have_content("Successfully saved") - - api.reload - - api.settings.rate_limits.length.should eql(1) - rate_limit = api.settings.rate_limits.first - rate_limit.limit.should eql(200) - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/config_publish_spec.rb b/src/api-umbrella/web-app/spec/features/admin/config_publish_spec.rb deleted file mode 100644 index 9cd1c5950..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/config_publish_spec.rb +++ /dev/null @@ -1,201 +0,0 @@ -require 'spec_helper' - -describe "config publish", :js => true do - login_admin - - before(:each) do - Api.delete_all - WebsiteBackend.delete_all - ConfigVersion.delete_all - end - - describe "review config changes" do - it "shows the pending configuration changes grouped into categories" do - FactoryGirl.create(:api) - deleted_api = FactoryGirl.create(:api) - modified_api = FactoryGirl.create(:api, :name => "Before") - ConfigVersion.publish!(ConfigVersion.pending_config) - deleted_api.destroy - modified_api.update_attribute(:name, "After") - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - page.should have_content("1 Deleted API Backends") - page.should have_content("1 Modified API Backends") - page.should have_content("1 New API Backends") - end - - it "hides the categories that have no changes" do - ConfigVersion.publish!(ConfigVersion.pending_config) - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - page.should_not have_content("Deleted API Backends") - page.should_not have_content("Modified API Backends") - page.should have_content("New API Backends") - end - - it "presents a message when there are no configuration changes to publish" do - FactoryGirl.create(:api) - ConfigVersion.publish!(ConfigVersion.pending_config) - - visit "/admin/#/config/publish" - page.should have_content("Published configuration is up to date") - end - - it "shows a diff view of the configuration" do - api = FactoryGirl.create(:api, :name => "Before") - ConfigVersion.publish!(ConfigVersion.pending_config) - api.update_attribute(:name, "After") - - visit "/admin/#/config/publish" - find(".config-diff", :visible => false).visible?.should eql(false) - click_link("View Config Differences") - find(".config-diff").visible?.should eql(true) - find(".config-diff del").text.should eql("Before") - find(".config-diff ins").text.should eql("After") - end - - it "selects the api for publishing by default if there is only one pending API" do - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - checkboxes = all("input[type=checkbox][name*=publish]") - checkboxes.length.should eql(1) - checkboxes.each do |checkbox| - checkbox[:checked].should eql(true) - end - end - - it "selects no apis for publishing by default if there is more than one pending API" do - FactoryGirl.create(:api) - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - checkboxes = all("input[type=checkbox][name*=publish]") - checkboxes.length.should eql(2) - checkboxes.each do |checkbox| - checkbox[:checked].should eql(false) - end - end - - it "refreshes the display when navigated away and back to" do - FactoryGirl.create(:api) - ConfigVersion.publish!(ConfigVersion.pending_config) - - visit "/admin/#/config/publish" - page.should_not have_content("New API Backends") - - visit "/admin/#/apis" - - FactoryGirl.create(:api) - visit "/admin/#/config/publish" - page.should have_content("1 New API Backends") - end - - it "provides a check/uncheck all link" do - FactoryGirl.create(:api) - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - - page.should have_content("Check all") - click_link("Check all") - checkboxes = all("input[type=checkbox][name*=publish]") - checkboxes.each do |checkbox| - checkbox[:checked].should eql(true) - end - - page.should have_content("Uncheck all") - click_link("Uncheck all") - checkboxes = all("input[type=checkbox][name*=publish]") - checkboxes.each do |checkbox| - checkbox[:checked].should eql(false) - end - - page.should have_content("Check all") - checkboxes = all("input[type=checkbox][name*=publish]") - checkboxes[0].click - page.should have_content("Check all") - checkboxes[1].click - page.should have_content("Uncheck all") - checkboxes[1].click - page.should have_content("Check all") - end - - it "disables the publish button if no changes are checked for publishing" do - FactoryGirl.create(:api) - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - - publish_button = find("#publish_button") - checkbox = all("input[type=checkbox][name*=publish]")[0] - - checkbox[:checked].should eql(false) - publish_button.disabled?.should eql(true) - - checkbox.click - checkbox[:checked].should eql(true) - publish_button.disabled?.should eql(false) - - checkbox.click - checkbox[:checked].should eql(false) - publish_button.disabled?.should eql(true) - end - - it "enables the publish button on load if the there's a single change pre-checked" do - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - - publish_button = find("#publish_button") - checkbox = all("input[type=checkbox][name*=publish]")[0] - - checkbox[:checked].should eql(true) - publish_button.disabled?.should eql(false) - - checkbox.click - checkbox[:checked].should eql(false) - publish_button.disabled?.should eql(true) - end - end - - describe "publish config changes" do - it "publishes the changes" do - api = FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - click_button("Publish") - - page.should have_content("Published configuration is up to date") - active_config = ConfigVersion.active_config - active_config["apis"].length.should eql(1) - active_config["apis"].first["_id"].should eql(api.id) - end - - it "displays a notification after successfully publishing" do - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - click_button("Publish") - - page.should have_content("Successfully published the configuration") - end - - it "publishes only the selected changes" do - api1 = FactoryGirl.create(:api) - FactoryGirl.create(:api) - - visit "/admin/#/config/publish" - check("config[apis][#{api1.id}][publish]") - click_button("Publish") - - page.should_not have_content("Published configuration is up to date") - page.should have_content("1 New API Backends") - active_config = ConfigVersion.active_config - active_config["apis"].length.should eql(1) - active_config["apis"].first["_id"].should eql(api1.id) - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/datatables_spec.rb b/src/api-umbrella/web-app/spec/features/admin/datatables_spec.rb deleted file mode 100644 index 80a7a662d..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/datatables_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' - -describe "datatables", :js => true do - login_admin - - before(:each) do - ApiUser.where(:registration_source.ne => "seed").delete_all - Api.delete_all - end - - describe "search" do - it "removes the default label" do - visit "/admin/#/api_users" - find(".dataTables_filter").text.should eql("") - end - - it "uses a placeholder" do - visit "/admin/#/api_users" - find(".dataTables_filter input")[:placeholder].should eql("Search...") - end - end - - describe "processing" do - it "displays a spinner on initial load" do - visit "/admin/#/api_users" - # We can't reliably check for the spinner on page load (it might - # disappear too quickly), so just ensure it eventualy disappears. - page.should_not have_selector(".dataTables_wrapper .blockOverlay") - page.should_not have_selector(".dataTables_wrapper .blockMsg") - end - - it "displays a spinner when server side processing" do - visit "/admin/#/api_users" - - # Slow down ajax queries so we can reliably have enough time to make sure - # that clicking a header triggers a server-side refresh which in turn - # should briefly show the spinner. - delay_all_ajax_calls - find("thead tr:first-child").click - page.should have_selector(".dataTables_wrapper .blockMsg .fa-spinner") - - # Ensure that the spinner eventually goes away after things load. - page.should_not have_selector(".dataTables_wrapper .blockOverlay") - page.should_not have_selector(".dataTables_wrapper .blockMsg") - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/elasticsearch_proxy.rb b/src/api-umbrella/web-app/spec/features/admin/elasticsearch_proxy.rb deleted file mode 100644 index d28e30f70..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/elasticsearch_proxy.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'spec_helper' - -describe "elasticsearch proxy", :js => true do - shared_examples "allowed access" do - end - - shared_examples "denied access" do - end - - describe "not logged in" do - it "redirects to the login page when accessing the root elasticsearch page" do - visit "/admin/elasticsearch" - page.current_url.should end_with("/admin/login") - page.should have_content("You need to sign in") - page.should_not have_content('"lucene_version"') - end - - it "redirects to the login page when trying to perform a basic elasticsearch query" do - visit "/admin/elasticsearch/_search" - page.current_url.should end_with("/admin/login") - page.should have_content("You need to sign in") - page.should_not have_content('"hits"') - end - end - - describe "logged in as limited admin" do - let(:current_admin) { FactoryGirl.create(:limited_admin) } - login_admin - - it "returns a not found error when accessing the root elasticsearch page" do - visit "/admin/elasticsearch" - page.status_code.should eql(404) - page.should_not have_content('"lucene_version"') - end - - it "returns a not found error when trying to perform a basic elasticsearch query" do - visit "/admin/elasticsearch/_search" - page.status_code.should eql(404) - page.should_not have_content('"hits"') - end - end - - describe "logged in as superuser" do - login_admin - - it "returns the root elasticsearch status page" do - visit "/admin/elasticsearch" - page.status_code.should eql(200) - page.should have_content('"lucene_version"') - end - - it "performs a basic elasticsearch query" do - visit "/admin/elasticsearch/_search" - page.status_code.should eql(200) - page.should have_content('"hits"') - end - - it "rewrites meta redirects returned by elasticsearch" do - visit "/admin/elasticsearch/_plugin/foobar" - page.body.should include("URL=/admin/elasticsearch/_plugin/foobar/") - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/locales_spec.rb b/src/api-umbrella/web-app/spec/features/admin/locales_spec.rb deleted file mode 100644 index 61438d353..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/locales_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require "spec_helper" - -describe "locales", :js => true do - describe "login page" do - I18n.available_locales.each do |locale| - it "translates in #{locale}" do - page.driver.add_headers("Accept-Language" => locale.to_s) - visit "/admin/login" - page.should have_content(I18n.t("omniauth_providers.developer", :locale => locale)) - end - - it "falls back to english for unknown languages" do - page.driver.add_headers("Accept-Language" => "zz") - visit "/admin/login" - page.should have_content(I18n.t("omniauth_providers.developer", :locale => "en")) - end - end - end - - describe "admin" do - login_admin - - describe "server-side and client-side js translations" do - I18n.available_locales.each do |locale| - it "translates in #{locale}" do - page.driver.add_headers("Accept-Language" => locale.to_s) - visit "/admin/#/apis/new" - - # Server-side rendered i18n - page.should have_content(I18n.t("admin.nav.analytics", :locale => locale)) - - # Client-side rendered i18n - page.should have_content(I18n.t("admin.api.servers.add", :locale => locale)) - end - - it "falls back to english for unknown languages" do - page.driver.add_headers("Accept-Language" => "zz") - visit "/admin/#/apis/new" - - # Server-side rendered i18n - page.should have_content(I18n.t("admin.nav.analytics", :locale => "en")) - - # Client-side rendered i18n - page.should have_content(I18n.t("admin.api.servers.add", :locale => "en")) - end - end - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/roles_spec.rb b/src/api-umbrella/web-app/spec/features/admin/roles_spec.rb deleted file mode 100644 index ede66da15..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/roles_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'spec_helper' - -describe "roles", :js => true do - login_admin - - before(:each) do - ApiUser.where(:registration_source.ne => "seed").delete_all - Api.delete_all - end - - describe "selectize options" do - it "prefills available options based on existing user roles" do - FactoryGirl.create(:api_user, :roles => ["test-user-role"]) - - visit "/admin/#/api_users/new" - find(".selectize-input").click - page.should have_content("test-user-role") - end - - it "prefills available options based on existing api roles" do - FactoryGirl.create(:api, :settings => { :required_roles => ["test-api-role"] }) - - visit "/admin/#/api_users/new" - find(".selectize-input").click - page.should have_content("test-api-role") - end - - it "refreshes prefill options with newly added roles during the current session" do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - find(".selectize-input input").set("test-new-role") - find(".selectize-dropdown-content div", :text => /Add test-new-role/).click - click_button("Save") - - page.should have_content("Successfully saved the user") - - click_link("Add New API User") - find(".selectize-input").click - page.should have_content("test-new-role") - end - - it "refreshes prefill options when roles are removed during the current session" do - user = FactoryGirl.create(:api_user, :roles => ["test-delete-role"]) - - visit "/admin/#/api_users/#{user.id}/edit" - find(".selectize-input div[data-value='test-delete-role'] a.remove").click - click_button("Save") - - page.should have_content("Successfully saved the user") - - click_link("Add New API User") - find(".selectize-input").click - page.should_not have_content("test-delete-role") - end - - it "shares role options between the api user form and the api backend forms" do - visit "/admin/#/api_users/new" - - fill_in "E-mail", :with => "example@example.com" - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Doe" - check "User agrees to the terms and conditions" - find(".selectize-input input").set("test-new-user-role") - find(".selectize-dropdown-content div", :text => /Add test-new-user-role/).click - click_button("Save") - - page.should have_content("Successfully saved the user") - - sleep 2 - visit "/admin/#/apis/new" - - sleep 2 - find("a", :text => /Global Request Settings/).click - find(".selectize-input").click - page.should have_content("test-new-user-role") - - find("a", :text => /Sub-URL Request Settings/).click - find("button", :text => /Add URL Settings/).click - find(".modal .selectize-input").click - page.should have_content("test-new-user-role") - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/stats_logs_spec.rb b/src/api-umbrella/web-app/spec/features/admin/stats_logs_spec.rb deleted file mode 100644 index 39943f977..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/stats_logs_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -require "spec_helper" -require "addressable/uri" -require 'test_helper/elasticsearch_helper' - -describe "analytics filter logs", :js => true do - login_admin - - before(:each) do - ElasticsearchHelper.clean_es_indices(["2014-11", "2015-01", "2015-03"]) - end - - describe "xss" do - it "escapes html entities in the table" do - log = FactoryGirl.create(:xss_log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :request_method => "OPTIONS") - LogItem.gateway.refresh_index! - - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day" - - page.should have_content(log.request_method) - page.should have_content(log.request_accept_encoding) - page.should have_content(log.request_ip_city) - page.should have_content(log.request_ip_country) - page.should have_content(log.request_ip_region) - page.should have_content(log.request_user_agent) - page.should have_content(log.response_content_type) - page.should have_content(log.user_email) - page.should_not have_selector(".xss-test", :visible => :all) - end - end - - describe "csv download" do - it "updates the download link as the query parameters change" do - FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z")) - LogItem.gateway.refresh_index! - default_query = JSON.generate({ - "condition" => "AND", - "rules" => [{ - "field" => "gatekeeper_denied_code", - "id" => "gatekeeper_denied_code", - "input" => "select", - "operator" => "is_null", - "type" => "string", - "value" => nil, - }] - }) - - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day" - wait_for_loading_spinners - page.should have_link("Download CSV", :href => /start_at=2015-01-12/) - link = find_link("Download CSV") - uri = Addressable::URI.parse(link[:href]) - uri.path.should eql("/admin/stats/logs.csv") - uri.query_values.should eql({ - "tz" => "America/Denver", - "search" => "", - "start_at" => "2015-01-12", - "end_at" => "2015-01-18", - "interval" => "day", - "query" => default_query, - "beta_analytics" => "false", - }) - - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-13&end_at=2015-01-18&interval=day" - wait_for_loading_spinners - page.should have_link("Download CSV", :href => /start_at=2015-01-13/) - link = find_link("Download CSV") - uri = Addressable::URI.parse(link[:href]) - uri.path.should eql("/admin/stats/logs.csv") - uri.query_values.should eql({ - "tz" => "America/Denver", - "search" => "", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - "query" => default_query, - "beta_analytics" => "false", - }) - - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day" - wait_for_loading_spinners - page.should have_link("Download CSV", :href => /start_at=2015-01-12/) - page.should have_link("Download CSV", :href => /%22rules%22%3A%5B%7B/) - click_button "Delete" # Remove the initial filter - click_button "Filter" - wait_for_loading_spinners - page.should have_link("Download CSV", :href => /%22rules%22%3A%5B%5D%7D/) - link = find_link("Download CSV") - uri = Addressable::URI.parse(link[:href]) - uri.path.should eql("/admin/stats/logs.csv") - uri.query_values.should eql({ - "tz" => "America/Denver", - "start_at" => "2015-01-12", - "end_at" => "2015-01-18", - "interval" => "day", - "query" => JSON.generate({ "condition" => "AND", "rules" => [] }), - "search" => "", - "beta_analytics" => "false", - }) - - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-13&end_at=2015-01-18&interval=day" - wait_for_loading_spinners - page.should have_link("Download CSV", :href => /start_at=2015-01-13/) - find("a", :text => /Switch to advanced filters/).click - fill_in "search", :with => "response_status:200" - click_button "Filter" - wait_for_loading_spinners - page.should have_link("Download CSV", :href => /response_status%3A200/) - link = find_link("Download CSV") - uri = Addressable::URI.parse(link[:href]) - uri.path.should eql("/admin/stats/logs.csv") - uri.query_values.should eql({ - "tz" => "America/Denver", - "search" => "response_status:200", - "start_at" => "2015-01-13", - "end_at" => "2015-01-18", - "interval" => "day", - "query" => "", - "beta_analytics" => "false", - }) - end - - it "successfully downloads a csv" do - FactoryGirl.create_list(:log_item, 5, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :request_method => "OPTIONS") - FactoryGirl.create_list(:log_item, 5, :request_at => 1421413588000, :request_method => "OPTIONS") - LogItem.gateway.refresh_index! - - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day" - wait_for_loading_spinners - - # Wait for the ajax actions to fetch the graph and tables to both - # complete, or else the download link seems to be flakey in Capybara. - page.should have_content("Download CSV") - page.should have_content("OPTIONS") - wait_for_ajax - click_link "Download CSV" - - # Downloading files via Capybara generally seems flakey, so add an extra - # wait. - wait_until { page.response_headers["Content-Type"] == "text/csv" } - page.status_code.should eql(200) - page.response_headers["Content-Type"].should eql("text/csv") - end - end - - describe "beta analytics option" do - it "doesn't show the beta analytics option by default" do - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day" - page.should_not have_content("Beta Analytics") - end - - it "shows the beta analytics toggle if kylin analytics are enabled" do - original = ApiUmbrellaConfig[:analytics][:outputs] - begin - ApiUmbrellaConfig[:analytics][:outputs] = ["kylin"] - visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day" - page.should have_content("Beta Analytics") - ensure - ApiUmbrellaConfig[:analytics][:outputs] = original - end - end - end -end diff --git a/src/api-umbrella/web-app/spec/features/admin/stats_users_spec.rb b/src/api-umbrella/web-app/spec/features/admin/stats_users_spec.rb deleted file mode 100644 index 0c9a6c2fa..000000000 --- a/src/api-umbrella/web-app/spec/features/admin/stats_users_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require "spec_helper" -require 'test_helper/elasticsearch_helper' - -describe "analytics by users", :js => true do - login_admin - - before(:each) do - ElasticsearchHelper.clean_es_indices(["2014-11", "2015-01", "2015-03"]) - end - - describe "xss" do - it "escapes html entities in the table" do - user = FactoryGirl.create(:xss_api_user) - FactoryGirl.create(:xss_log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :api_key => user.api_key, :user_id => user.id, :user_email => user.email, :user_registration_source => user.registration_source) - LogItem.gateway.refresh_index! - - visit "/admin/#/stats/users/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day" - - page.should have_content(user.email) - page.should have_content(user.first_name) - page.should have_content(user.last_name) - page.should have_content(user.use_description) - page.should_not have_selector(".xss-test", :visible => :all) - end - end -end diff --git a/src/api-umbrella/web-app/spec/helpers/datatables_helper_spec.rb b/src/api-umbrella/web-app/spec/helpers/datatables_helper_spec.rb deleted file mode 100644 index 1f08774a2..000000000 --- a/src/api-umbrella/web-app/spec/helpers/datatables_helper_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'spec_helper' - -describe DatatablesHelper do - describe "#datatables_sort" do - it "uses defaults to no order" do - helper.stub(:params) { {} } - expect(helper.datatables_sort).to eq([]) - end - - it "orders per request" do - helper.stub(:params) do - { :columns => { "0" => { :data => 'col0' }, "1" => { :data => 'col1' }, - "2" => { :data => 'col2' }, "3" => { :data => 'col3' } }, - :order => [{ :column => '2', :dir => 'asc' }, { :column => '0', :dir => 'desc' }] - } - end - expect(helper.datatables_sort).to eq([ - { 'col2' => 'asc' }, { 'col0' => 'desc' }]) - end - end - - describe "#param_index_array" do - it "doesn't touch a parameter that's already an array" do - helper.stub(:params) { { :key => ["a", "b", "c"] } } - expect(helper.param_index_array(:key)).to eq(["a", "b", "c"]) - end - - it "converts parameters to arrays" do - helper.stub(:params) { { :key => "value" } } - expect(helper.param_index_array(:key)).to eq(["value"]) - end - - it "given an empty array when the parameter is not present" do - helper.stub(:params) { {} } - expect(helper.param_index_array(:key)).to eq([]) - end - - it "converts arrays with object indexes" do - helper.stub(:params) { { :key => { "0" => "a", "1" => "b", "2" => "c" } } } - expect(helper.param_index_array(:key)).to eq(["a", "b", "c"]) - end - - it "does not explode with missing indexes" do - helper.stub(:params) { { :key => { "0" => "a", "2" => "c" } } } - expect(helper.param_index_array(:key)).to eq(["a"]) - end - - it "does not explode with mixed indexes" do - helper.stub(:params) { { :key => { "0" => "a", "str" => "b", "2" => "c" } } } - expect(helper.param_index_array(:key)).to eq(["a", "c"]) - end - end - - describe "#datatables_columns" do - it "pulls out column data under realistic conditions" do - helper.stub(:params) do - { :columns => { - "0" => { :data => "username", :name => "Username", :searchable => true, - :orderable => true, :search => { :value => '', :regex => false } }, - "1" => { :data => "email", :name => "E-mail", :searchable => true, - :orderable => true, :search => { :value => '', :regex => false } }, - "2" => { :data => "name", :name => "Name", :searchable => true, - :orderable => true, :search => { :value => '', :regex => false } }, - } } - end - expect(helper.datatables_columns).to eq([ - { :name => 'Username', :field => 'username' }, - { :name => 'E-mail', :field => 'email' }, - { :name => 'Name', :field => 'name' }]) - end - - it "accounts for data errors" do - helper.stub(:params) do - { :columns => { - "0" => { :name => "Username", :searchable => true, # missing data - :orderable => true, :search => { :value => '', :regex => false } }, - "1" => { :data => "email", :searchable => true, # missing name - :orderable => true, :search => { :value => '', :regex => false } }, - "2" => { :data => ["a", "b", "c"], :name => "Name", :searchable => true, # data is an array - :orderable => true, :search => { :value => '', :regex => false } }, - } } - end - expect(helper.datatables_columns).to eq([ - { :name => '-', :field => 'email' }, - { :name => 'Name', :field => '["a", "b", "c"]' }]) - end - end - - describe "#csv_output" do - it "generates a csv" do - results = [{ "a" => 1, "b" => 2, "c" => 3 }, { "a" => 4, "b" => 5, "c" => 6 }] - columns = [{ :name => "A", :field => "a" }, { :name => "B", :field => "b" }, { :name => "C", :field => "c" }] - output = helper.csv_output(results, columns) - expect(output).to eq("A,B,C\n1,2,3\n4,5,6\n") - end - - it "does not include fields not requested" do - results = [{ "a" => 1, "b" => 2, "c" => 3 }, { "a" => 4, "b" => 5, "c" => 6 }] - columns = [{ :name => "A", :field => "a" }, { :name => "C", :field => "c" }] - output = helper.csv_output(results, columns) - expect(output).to eq("A,C\n1,3\n4,6\n") - end - - it "skips over missing fields" do - results = [{ "a" => 1, "c" => 3 }, { "a" => 4, "b" => 5, "c" => 6 }] - columns = [{ :name => "A", :field => "a" }, { :name => "B", :field => "b" }, { :name => "C", :field => "c" }] - output = helper.csv_output(results, columns) - expect(output).to eq("A,B,C\n1,,3\n4,5,6\n") - end - - it "converts lists to a string" do - results = [{ "a" => 1, "b" => [2, 3, 4] }] - columns = [{ :name => "A", :field => "a" }, { :name => "B", :field => "b" }] - output = helper.csv_output(results, columns) - expect(output).to eq("A,B\n1," + '"2,3,4"' + "\n") - end - end - - describe "#respond_to_datatables" # @todo -end diff --git a/src/api-umbrella/web-app/spec/mailers/api_user_mailer_spec.rb b/src/api-umbrella/web-app/spec/mailers/api_user_mailer_spec.rb deleted file mode 100644 index 031394e3e..000000000 --- a/src/api-umbrella/web-app/spec/mailers/api_user_mailer_spec.rb +++ /dev/null @@ -1,138 +0,0 @@ -require "spec_helper" - -describe ApiUserMailer do - describe "signup_email" do - describe "OSVDB-131677 security" do - it "accepts recipients without newlines" do - expect do - api_user = FactoryGirl.create(:api_user, :email => "foo@example.com") - ApiUserMailer.signup_email(api_user, {}).deliver - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "rejects recipients with newlines" do - expect do - expect do - api_user = FactoryGirl.create(:api_user, :email => "foo@example.com\nfoo") - ApiUserMailer.signup_email(api_user, {}).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "rejects recipients with carriage returns" do - expect do - expect do - api_user = FactoryGirl.create(:api_user, :email => "foo@example.com\rfoo") - ApiUserMailer.signup_email(api_user, {}).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "accepts recipients 500 chars or less" do - expect do - api_user = FactoryGirl.create(:api_user, :email => "#{"o" * 488}@example.com") - ApiUserMailer.signup_email(api_user, {}).deliver - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "rejects recipients greater than 500 chars" do - expect do - expect do - api_user = FactoryGirl.create(:api_user, :email => "#{"o" * 489}@example.com") - ApiUserMailer.signup_email(api_user, {}).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "accepts from addresses without newlines" do - expect do - api_user = FactoryGirl.create(:api_user) - ApiUserMailer.signup_email(api_user, { :email_from_address => "foo@example.com" }).deliver - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "rejects from addresses with newlines" do - expect do - expect do - api_user = FactoryGirl.create(:api_user) - ApiUserMailer.signup_email(api_user, { :email_from_address => "foo@example.com\nfoo" }).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "rejects from addresses with carriage returns" do - expect do - expect do - api_user = FactoryGirl.create(:api_user) - ApiUserMailer.signup_email(api_user, { :email_from_address => "foo@example.com\rfoo" }).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "accepts from addresses 500 chars or less" do - expect do - api_user = FactoryGirl.create(:api_user) - ApiUserMailer.signup_email(api_user, { :email_from_address => "#{"o" * 488}@example.com" }).deliver - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "rejects from addresses greater than 500 chars" do - expect do - expect do - api_user = FactoryGirl.create(:api_user) - ApiUserMailer.signup_email(api_user, { :email_from_address => "#{"o" * 489}@example.com" }).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - end - end - - describe "signup_email" do - before(:each) do - ApiUmbrellaConfig[:web][:contact_form_email] = "aaa@bbb.com" - ApiUmbrellaConfig[:web][:default_host] = "localhost.com" - end - - let(:api_user) do - FactoryGirl.create( - :api_user, - :first_name => "aaa", - :last_name => "bbb", - :use_description => "I WANNA DO EVERYTHING", - :email => "foo@example.com") - end - - subject { ApiUserMailer.notify_api_admin(api_user).deliver } - - it "send an email " do - expect { subject }.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "send an email to the contact email" do - subject - expect(ActionMailer::Base.deliveries.first.to).to eq ["aaa@bbb.com"] - end - - it "the receiver can be overwrited by the admin " do - ApiUmbrellaConfig[:web][:admin_notify_email] = "ccc@ddd.com" - subject - expect(ActionMailer::Base.deliveries.first.to).to eq ["ccc@ddd.com"] - end - - it "send an email with the name of the person in the subject" do - subject - expect(ActionMailer::Base.deliveries.first.subject).to eq "aaa bbb just subscribed" - end - - it "send an email from the server name" do - subject - expect(ActionMailer::Base.deliveries.first.from).to eq ["noreply@localhost.com"] - end - - it "send an email with usage in the body" do - subject - expect(ActionMailer::Base.deliveries.first.encoded).to include "I WANNA DO EVERYTHING" - end - end -end diff --git a/src/api-umbrella/web-app/spec/mailers/contact_mailer_spec.rb b/src/api-umbrella/web-app/spec/mailers/contact_mailer_spec.rb deleted file mode 100644 index 9cf741222..000000000 --- a/src/api-umbrella/web-app/spec/mailers/contact_mailer_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require "spec_helper" - -describe ContactMailer do - describe "OSVDB-131677 security" do - before(:each) do - ApiUmbrellaConfig[:web][:contact_form_email] = "test@example.com" - @contact = Contact.new({ - :name => "John Doe", - :api => "Foo", - :subject => "Bar", - :message => "Hello, World", - }) - end - - it "accepts addresses without newlines" do - expect do - @contact.email = "foo@example.com" - ContactMailer.contact_email(@contact).deliver - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "rejects addresses with newlines" do - expect do - expect do - @contact.email = "foo@example.com\nfoo" - ContactMailer.contact_email(@contact).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "rejects addresses with carriage returns" do - expect do - expect do - @contact.email = "foo@example.com\rfoo" - ContactMailer.contact_email(@contact).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - - it "accepts addresses 500 chars or less" do - expect do - @contact.email = "#{"o" * 488}@example.com" - ContactMailer.contact_email(@contact).deliver - end.to change { ActionMailer::Base.deliveries.count }.by(1) - end - - it "rejects addresses greater than 500 chars" do - expect do - expect do - @contact.email = "#{"o" * 489}@example.com" - ContactMailer.contact_email(@contact).deliver - end.to raise_error(MailSanitizer::InvalidAddress) - end.to change { ActionMailer::Base.deliveries.count }.by(0) - end - end -end diff --git a/src/api-umbrella/web-app/spec/models/api_spec.rb b/src/api-umbrella/web-app/spec/models/api_spec.rb deleted file mode 100644 index 783f72258..000000000 --- a/src/api-umbrella/web-app/spec/models/api_spec.rb +++ /dev/null @@ -1,292 +0,0 @@ -require 'spec_helper' - -describe Api do - context "host validations" do - shared_examples "valid host" do - it "passes validations" do - api.valid?.should eql(true) - end - end - - shared_examples "invalid host" do - it "fails validations" do - api.valid?.should eql(false) - api.errors.messages.keys.sort.should eql([ - :backend_host, - :frontend_host, - :"servers[0].host" - ]) - - api.errors_on(:backend_host).should include('must be in the format of "example.com"') - api.errors_on(:frontend_host).should include('must be in the format of "example.com"') - api.errors_on(:"servers[0].host").should include('must be in the format of "example.com"') - end - end - - describe "accepts a external hostname" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "example.com", - :backend_host => "example.com", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "example.com"), - ] - }) - end - - it_behaves_like "valid host" - end - - describe "accepts a internal hostname" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "localhost", - :backend_host => "localhost", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "localhost"), - ] - }) - end - - it_behaves_like "valid host" - end - - describe "accepts an IPv4 address" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "127.0.0.1", - :backend_host => "127.0.0.1", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - end - - it_behaves_like "valid host" - end - - describe "accepts a compacted IPv6 address" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "::1", - :backend_host => "::1", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "::1"), - ] - }) - end - - it_behaves_like "valid host" - end - - describe "accepts a full IPv6 address" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "2001:db8:85a3::8a2e:370:7334", - :backend_host => "2001:db8:85a3::8a2e:370:7334", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "2001:db8:85a3::8a2e:370:7334"), - ] - }) - end - - it_behaves_like "valid host" - end - - describe "rejects a hostname with a protocol prefix" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "http://example.com", - :backend_host => "http://example.com", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "http://example.com"), - ] - }) - end - - it_behaves_like "invalid host" - end - - describe "rejects a hostname with a trailing slash" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "example.com/", - :backend_host => "example.com/", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "example.com/"), - ] - }) - end - - it_behaves_like "invalid host" - end - - describe "rejects a hostname with a path suffix" do - let(:api) do - FactoryGirl.build(:api, { - :frontend_host => "example.com/test", - :backend_host => "example.com/test", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "example.com/test"), - ] - }) - end - - it_behaves_like "invalid host" - end - - it "allows a wildcard for the frontend host or backend host, but not for server host" do - api = FactoryGirl.build(:api, { - :frontend_host => "*", - :backend_host => "*", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "*"), - ] - }) - - api.valid?.should eql(false) - api.errors.messages.keys.sort.should eql([ - :"servers[0].host" - ]) - - api.errors_on(:"servers[0].host").should include('must be in the format of "example.com"') - end - - it "allows an empty string backend host when the frontend host is a wildcard" do - api = FactoryGirl.build(:api, { - :frontend_host => "*", - :backend_host => "", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(true) - end - - it "allows an null backend host when the frontend host is a wildcard" do - api = FactoryGirl.build(:api, { - :frontend_host => "*", - :backend_host => nil, - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(true) - end - - it "allows an empty backend host when the frontend host contains a wildcard with dot" do - api = FactoryGirl.build(:api, { - :frontend_host => "*.example.com", - :backend_host => nil, - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(true) - end - - it "does not allow a frontend or backend host starting with a wildcard when not bordered by a dot" do - api = FactoryGirl.build(:api, { - :frontend_host => "*example.com", - :backend_host => "*example.com", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(false) - api.errors.messages.keys.sort.should eql([ - :backend_host, - :frontend_host, - ]) - end - - it "allows a frontend or backend host starting with a star dot" do - api = FactoryGirl.build(:api, { - :frontend_host => "*.example.com", - :backend_host => "*.example.com", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(true) - end - - it "allows a frontend or backend host starting with a dot" do - api = FactoryGirl.build(:api, { - :frontend_host => ".example.com", - :backend_host => ".example.com", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(true) - end - - it "does not allow a frontend host equal to '.'" do - api = FactoryGirl.build(:api, { - :frontend_host => ".", - :backend_host => "example.com", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(false) - api.errors.messages.keys.sort.should eql([ - :frontend_host, - ]) - end - - it "does not allow a frontend host equal to '*.'" do - api = FactoryGirl.build(:api, { - :frontend_host => "*.", - :backend_host => "example.com", - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(false) - api.errors.messages.keys.sort.should eql([ - :frontend_host, - ]) - end - - it "does not allow an empty backend host when the frontend host does not contain a wildcard" do - api = FactoryGirl.build(:api, { - :frontend_host => "example.com", - :backend_host => nil, - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(false) - api.errors.messages.keys.sort.should eql([ - :backend_host, - ]) - end - - it "does not allow an empty backend host when the frontend host contains a wildcard in the middle" do - api = FactoryGirl.build(:api, { - :frontend_host => "exam*ple.com", - :backend_host => nil, - :servers => [ - FactoryGirl.attributes_for(:api_server, :host => "127.0.0.1"), - ] - }) - - api.valid?.should eql(false) - api.errors.messages.keys.sort.should eql([ - :backend_host, - :frontend_host, - ]) - end - end -end diff --git a/src/api-umbrella/web-app/spec/models/api_user_spec.rb b/src/api-umbrella/web-app/spec/models/api_user_spec.rb deleted file mode 100644 index cb6f86fe1..000000000 --- a/src/api-umbrella/web-app/spec/models/api_user_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe ApiUser do - context "api key generation" do - before(:all) do - @api_user = FactoryGirl.create(:api_user, :api_key => nil) - end - - it "generates a new key on create" do - @api_user.api_key.should_not eq(nil) - end - - it "contains only A-Z, a-z, and 0-9 chars" do - @api_user.api_key.should match(/^[0-9A-Za-z]+$/) - end - - it "is 40 characters long" do - @api_user.api_key.length.should eq(40) - end - end -end diff --git a/src/api-umbrella/web-app/spec/requests/api/v1/users_spec.rb b/src/api-umbrella/web-app/spec/requests/api/v1/users_spec.rb deleted file mode 100644 index cd217f760..000000000 --- a/src/api-umbrella/web-app/spec/requests/api/v1/users_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require "spec_helper" - -describe "/api/v1/users" do - # Make sure our API key creation endpoint can be successfully called with - # IE8-9's shimmed pseudo-CORS support. This ensures API keys can be created - # even if the endpoint is called with empty or text/plain content-types. See - # ApplicationController#parse_post_for_pseudo_ie_cors for more detail. - describe "IE8-9 pseudo-CORS compatibility" do - let(:url) { "/api/v1/users.json?api_key=DEMO_KEY" } - let(:headers) do - { - "HTTP_X_API_ROLES" => "api-umbrella-key-creator", - } - end - - let(:post_data) do - { - :user => { - :first_name => "Mr", - :last_name => "Potato", - :email => "potato@example.com", - :use_description => "", - :terms_and_conditions => "1", - }, - } - end - - it "accepts form data with a nil content-type" do - expect do - post url, post_data, headers.merge("CONTENT_TYPE" => nil) - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["user"]["last_name"].should eql("Potato") - - user = ApiUser.find(data["user"]["id"]) - user.last_name.should eql("Potato") - end.to change { ApiUser.count }.by(1) - end - - it "accepts form data with a empty content-type" do - expect do - post url, post_data, headers.merge("CONTENT_TYPE" => "") - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["user"]["last_name"].should eql("Potato") - - user = ApiUser.find(data["user"]["id"]) - user.last_name.should eql("Potato") - end.to change { ApiUser.count }.by(1) - end - - it "accepts form data a content-type of text/plain" do - expect do - post url, post_data, headers.merge("CONTENT_TYPE" => "text/plain") - - response.status.should eql(201) - data = MultiJson.load(response.body) - data["user"]["last_name"].should eql("Potato") - - user = ApiUser.find(data["user"]["id"]) - user.last_name.should eql("Potato") - end.to change { ApiUser.count }.by(1) - end - end -end diff --git a/src/api-umbrella/web-app/spec/spec_helper.rb b/src/api-umbrella/web-app/spec/spec_helper.rb deleted file mode 100644 index 5a38df986..000000000 --- a/src/api-umbrella/web-app/spec/spec_helper.rb +++ /dev/null @@ -1,37 +0,0 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' -ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../../config/environment", __FILE__) -require 'rspec/rails' - -# Requires supporting ruby files with custom matchers and macros, etc, -# in spec/support/ and its subdirectories. -# -# Make sure the integration start processes support file gets required first, -# so it has an opportunity to set up it's callbacks to start processes first. -require Rails.root.join("spec/support/integration_start_processes.rb") -Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } - -# Prevent test-unit from trying to run after running rspec tests manually: -# https://github.com/grosser/parallel_tests/issues/189 -# -# TODO: This is likely due to our test-unit dependency in our Gemfile due to -# Rails 3.2 and Ruby 2.2 compatibility. Revisit this when we upgrade Rails. -Test::Unit::AutoRunner.need_auto_run = false if defined?(Test::Unit::AutoRunner) - -RSpec.configure do |config| - # == Mock Framework - # - # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: - # - # config.mock_with :mocha - # config.mock_with :flexmock - # config.mock_with :rr - config.mock_with :rspec - - config.infer_spec_type_from_file_location! - - # Don't filter backtraces - config.backtrace_exclusion_patterns = [] - - config.render_views -end diff --git a/src/api-umbrella/web-app/spec/support/capybara.rb b/src/api-umbrella/web-app/spec/support/capybara.rb deleted file mode 100644 index 58794277f..000000000 --- a/src/api-umbrella/web-app/spec/support/capybara.rb +++ /dev/null @@ -1,75 +0,0 @@ -require "capybara/rspec" -require "capybara/rails" -require "capybara/poltergeist" -require "capybara-screenshot/rspec" - -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, { - :phantomjs_logger => File.open("#{Rails.root}/log/test_phantomjs.log", "a"), - }) -end - -Capybara.javascript_driver = :poltergeist - -# Set a longer timeout for places like TravisCI where things can sometimes be -# slower. -Capybara.default_max_wait_time = 15 - -module CapybaraFeatureHelpers - def wait_for_ajax - Timeout.timeout(Capybara.default_max_wait_time) do - loop until finished_all_ajax_requests? - end - end - - def finished_all_ajax_requests? - page.evaluate_script('jQuery.active').zero? - end - - def wait_for_datatables_filter - sleep 1 - end - - def wait_for_loading_spinners - page.should_not have_selector(".loading-overlay .spinner") - page.should_not have_selector(".dataTables_wrapper .blockOverlay") - page.should_not have_selector(".dataTables_wrapper .blockMsg") - end - - def wait_until - require "timeout" - Timeout.timeout(Capybara.default_max_wait_time) do - sleep(0.1) until(value = yield) # rubocop:disable Lint/AssignmentInCondition - value - end - end - - def delay_all_ajax_calls(delay = 1500) - page.execute_script <<-eos - $.ajaxOrig = $.ajax; - $.ajax = function() { - var args = arguments; - var self = this; - setTimeout(function() { - $.ajaxOrig.apply(self, args); - }, #{delay}); - }; - eos - end -end - -RSpec.configure do |config| - config.include CapybaraFeatureHelpers, :type => :feature - - config.before(:each, :type => :feature) do - # Set the default language for tests to US English. - # - # This ensures we have a consistent baseline for testing, regardless of the - # default language on the computer running tests. See: - # https://github.com/NREL/api-umbrella/issues/242 - # - # We then explicitly test the support of other languages by overriding this - # in spec/features/admin/locales_spec.rb - page.driver.add_headers("Accept-Language" => "en-US") - end -end diff --git a/src/api-umbrella/web-app/spec/support/database_setup.rb b/src/api-umbrella/web-app/spec/support/database_setup.rb deleted file mode 100644 index 95877cde3..000000000 --- a/src/api-umbrella/web-app/spec/support/database_setup.rb +++ /dev/null @@ -1,13 +0,0 @@ -RSpec.configure do |config| - config.before(:suite) do - DatabaseCleaner[:mongoid].strategy = :truncation - DatabaseCleaner.clean - SeedFu.seed - - # For future cleaner calls, remove everything except the permissions we've - # seeded. - DatabaseCleaner[:mongoid].strategy = :truncation, { - :except => ["admin_permissions"], - } - end -end diff --git a/src/api-umbrella/web-app/spec/support/devise.rb b/src/api-umbrella/web-app/spec/support/devise.rb deleted file mode 100644 index 475152233..000000000 --- a/src/api-umbrella/web-app/spec/support/devise.rb +++ /dev/null @@ -1,60 +0,0 @@ -module DeviseControllerMacros - def login_admin - before(:each) do - @current_admin = if(defined?(current_admin)) then current_admin else FactoryGirl.create(:admin) end - admin_login_auth(@current_admin) - end - end -end - -module DeviseControllerHelpers - def admin_token_auth(admin) - request.env["HTTP_X_ADMIN_AUTH_TOKEN"] = admin.authentication_token - end - - def admin_login_auth(admin) - @request.env["devise.mapping"] = Devise.mappings[:admin] - sign_in admin - end -end - -module DeviseFeatureMacros - def login_admin - before(:each) do - @current_admin = if(defined?(current_admin)) then current_admin else FactoryGirl.create(:admin) end - - Warden.test_mode! - login_as(@current_admin, :scope => :admin) - - # FIXME: When running standalone feature tests, sometimes the first test - # inexplicably fails. The pages appear to load, but interacting with the - # forms and trying to save fail (it's almost like the javascript hasn't - # fully loaded if things aren't warmed up). However, this issue goes away - # on subsequent tests, so the super hacky workaround is to load the index - # page once before any tests and that seems to fix the issue. We should - # revisit sometime to try to figure out if this is a Capybara/poltergeist - # issue or something with our app. - # - # rubocop:disable Style/GlobalVars - unless($admin_loaded_once) - visit "/admin/" - page.should have_content("API Umbrella") - $admin_loaded_once = true - end - # rubocop:enable Style/GlobalVars - end - - after(:each) do - Warden.test_reset! - end - end -end - -RSpec.configure do |config| - config.include Devise::TestHelpers, :type => :controller - config.include DeviseControllerHelpers, :type => :controller - config.extend DeviseControllerMacros, :type => :controller - - config.include Warden::Test::Helpers, :type => :feature - config.extend DeviseFeatureMacros, :type => :feature -end diff --git a/src/api-umbrella/web-app/spec/support/elasticsearch.rb b/src/api-umbrella/web-app/spec/support/elasticsearch.rb deleted file mode 100644 index 9da31bef3..000000000 --- a/src/api-umbrella/web-app/spec/support/elasticsearch.rb +++ /dev/null @@ -1,38 +0,0 @@ -require "support/vcr" - -RSpec.configure do |config| - config.before(:suite) do - client = Elasticsearch::Client.new({ - :hosts => ApiUmbrellaConfig[:elasticsearch][:hosts], - :logger => Rails.logger - }) - - templates = MultiJson.load(File.read(File.expand_path("../../../../../../config/elasticsearch_templates.json", __FILE__))) - templates.each do |template| - client.indices.put_template({ - :name => template["id"], - :body => template["template"], - }) - end - - # For simplicity sake, we're assuming our tests only deal with a few explicit - # indexes currently. - ["2014-11", "2015-01", "2015-03"].each do |month| - # First delete any existing indexes. - ["api-umbrella-logs-v1-#{month}", "api-umbrella-logs-#{month}", "api-umbrella-logs-write-#{month}"].each do |index_name| - begin - client.indices.delete :index => index_name - rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/HandleExceptions - end - end - - # Create the index with proper aliases setup. - client.indices.create(:index => "api-umbrella-logs-v1-#{month}", :body => { - :aliases => { - "api-umbrella-logs-#{month}" => {}, - "api-umbrella-logs-write-#{month}" => {}, - }, - }) - end - end -end diff --git a/src/api-umbrella/web-app/spec/support/integration_start_processes.rb b/src/api-umbrella/web-app/spec/support/integration_start_processes.rb deleted file mode 100644 index 046d3d2c6..000000000 --- a/src/api-umbrella/web-app/spec/support/integration_start_processes.rb +++ /dev/null @@ -1,55 +0,0 @@ -RSpec.configure do |config| - # If we're explicitly running the Rails test suite as part of the integration - # tests, do things a bit differently, and startup the databases as separate - # processes via the normal API Umbrella start command. - # - # This ensures that everything is compatible with the actual bundled versions - # of the database servers and is similar to how we startup API Umbrella as a - # sub-process of the tests in the other npm integration tests. - if(ENV["INTEGRATION_TEST_SUITE"]) - # rubocop:disable Style/GlobalVars - config.before(:suite) do - root = File.expand_path("../../../../../../", __FILE__) - embedded_root = File.join(root, "build/work/stage/opt/api-umbrella/embedded") - - # Pull in the test.yml config file used in all the other npm integration - # tests. Alter to prevent running of the web app (since that's what we're - # testing here, so it doesn't need to be spun up), and for these test - # purposes, just connect to the single mongodb instance (not the - # replicaset the npm tests run against). - config = YAML.load_file(File.join(root, "test/config/test.yml")) - config["services"] = ["general_db", "log_db", "router"] - config["mongodb"]["url"] = "mongodb://127.0.0.1:13001/api_umbrella_test" - config_path = Rails.root.join("tmp/integration_test_suite.yml") - File.write(config_path, YAML.dump(config)) - - # Spin up API Umbrella and the embedded databases as a background - # process. - $api_umbrella_process = ChildProcess.build(File.join(root, "bin/api-umbrella"), "run") - $api_umbrella_process.io.inherit! - $api_umbrella_process.environment["API_UMBRELLA_EMBEDDED_ROOT"] = embedded_root - $api_umbrella_process.environment["API_UMBRELLA_CONFIG"] = config_path - $api_umbrella_process.start - - # Run the health command to wait for API Umbrella to fully startup. - health = ChildProcess.build(File.join(root, "bin/api-umbrella"), "health", "--wait-for-status", "green", "--wait-timeout", "90") - health.io.inherit! - health.environment["API_UMBRELLA_EMBEDDED_ROOT"] = embedded_root - health.environment["API_UMBRELLA_CONFIG"] = config_path - health.start - health.wait - - # If anything exited unsuccessfully, abort tests. - if(health.crashed? || $api_umbrella_process.crashed?) - raise "Did not start api-umbrella process for integration tests" - end - end - - config.after(:suite) do - if($api_umbrella_process && $api_umbrella_process.alive?) - $api_umbrella_process.stop - end - end - # rubocop:enable Style/GlobalVars - end -end diff --git a/src/api-umbrella/web-app/spec/support/matchers/uuid.rb b/src/api-umbrella/web-app/spec/support/matchers/uuid.rb deleted file mode 100644 index c94683a3f..000000000 --- a/src/api-umbrella/web-app/spec/support/matchers/uuid.rb +++ /dev/null @@ -1,5 +0,0 @@ -RSpec::Matchers.define :be_a_uuid do - match do |actual| - actual =~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ - end -end diff --git a/src/api-umbrella/web-app/spec/support/shared_examples/admin_permissions.rb b/src/api-umbrella/web-app/spec/support/shared_examples/admin_permissions.rb deleted file mode 100644 index 7665b60b9..000000000 --- a/src/api-umbrella/web-app/spec/support/shared_examples/admin_permissions.rb +++ /dev/null @@ -1,232 +0,0 @@ -shared_examples "admin permissions" do |options| - options ||= {} - - let(:except_required_permissions) do - AdminPermission.pluck(:id) - options[:required_permissions] - end - - describe "superuser" do - before(:each) do - @admin = FactoryGirl.create(:admin) - end - it_behaves_like "admin permitted" - end - - describe "localhost/* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:localhost_root_admin_group), - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/* admin with only #{options[:required_permissions].join(", ")} permissions" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:localhost_root_admin_group, :permission_ids => options[:required_permissions]), - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/* admin with all permissions except #{options[:required_permissions].join(", ")}" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:localhost_root_admin_group, :permission_ids => except_required_permissions), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/z* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:localhost_root_api_scope, :path_prefix => "/z")), - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:google_admin_group), - ]) - end - if(options[:root_required]) - it_behaves_like "admin forbidden" - else - it_behaves_like "admin permitted" - end - end - - describe "localhost/google* admin with only #{options[:required_permissions].join(", ")} permissions" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:google_admin_group, :permission_ids => options[:required_permissions]), - ]) - end - if(options[:root_required]) - it_behaves_like "admin forbidden" - else - it_behaves_like "admin permitted" - end - end - - describe "localhost/google* admin with all permissions except #{options[:required_permissions].join(", ")}" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:google_admin_group, :permission_ids => except_required_permissions), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhost/googl* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope, :path_prefix => "/googl")), - ]), - ]) - end - if(options[:root_required]) - it_behaves_like "admin forbidden" - else - it_behaves_like "admin permitted" - end - end - - describe "localhost/googlez* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope, :path_prefix => "/googlez")), - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhos/* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:localhost_root_admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:localhost_root_api_scope, :host => "localhos")), - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhostz/* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:localhost_root_admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:localhost_root_api_scope, :host => "localhostz")), - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhos/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope, :host => "localhos")), - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "localhostz/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope, :host => "localhostz")), - ]), - ]) - end - it_behaves_like "admin forbidden" - end - - describe "multi-scope groups with overlapping scopes exist" do - before(:each) do - @localhost_root_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:localhost_root_api_scope)) - @google_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:google_api_scope)) - @yahoo_api_scope = ApiScope.find_or_create_by_instance!(FactoryGirl.build(:yahoo_api_scope)) - @multi_root_scope_group = FactoryGirl.create(:admin_group, :api_scopes => [ - @localhost_root_api_scope, - @google_api_scope, - ]) - @multi_sub_scope_group = FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - @yahoo_api_scope, - ]) - end - - describe "localhost/* and localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - @multi_root_scope_group, - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* and localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - @multi_sub_scope_group, - ]) - end - if(options[:root_required]) - it_behaves_like "admin forbidden" - else - it_behaves_like "admin permitted" - end - end - - describe "localhost/* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @localhost_root_api_scope, - ]), - ]) - end - it_behaves_like "admin permitted" - end - - describe "localhost/google* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @google_api_scope, - ]), - ]) - end - if(options[:root_required]) - it_behaves_like "admin forbidden" - else - it_behaves_like "admin permitted" - end - end - - describe "localhost/yahoo* full admin" do - before(:each) do - @admin = FactoryGirl.create(:limited_admin, :groups => [ - FactoryGirl.create(:admin_group, :api_scopes => [ - @yahoo_api_scope, - ]), - ]) - end - it_behaves_like "admin forbidden" - end - end -end diff --git a/src/api-umbrella/web-app/spec/support/vcr.rb b/src/api-umbrella/web-app/spec/support/vcr.rb deleted file mode 100644 index 3324ac9e0..000000000 --- a/src/api-umbrella/web-app/spec/support/vcr.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "vcr" - -VCR.configure do |c| - c.cassette_library_dir = "#{::Rails.root}/spec/cassettes" - c.hook_into :webmock - c.default_cassette_options = { - :record => :new_episodes, - - # Store gzip responses as plaintext in the YAML. - :decode_compressed_response => true, - - # Allow the same response to be used multiple times in a single test. - :allow_playback_repeats => true, - } - - # Allow localhost connections for ElasticSearch. - c.ignore_localhost = true - - c.configure_rspec_metadata! -end diff --git a/src/api-umbrella/web-app/spec/test_helper/elasticsearch_helper.rb b/src/api-umbrella/web-app/spec/test_helper/elasticsearch_helper.rb deleted file mode 100644 index bd119ecd8..000000000 --- a/src/api-umbrella/web-app/spec/test_helper/elasticsearch_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -class ElasticsearchHelper - def self.clean_es_indices(indices) - indices.each do |month| - result = LogItem.gateway.client.search :index => "api-umbrella-logs-#{month}", - :body => { - :query => { - :match_all => {} - }, - :size => 1000, - } - bulk_request = result["hits"]["hits"].map do |hit| - { :delete => { :_index => hit["_index"], :_type => hit["_type"], :_id => hit["_id"] } } - end - - unless bulk_request.empty? - LogItem.gateway.client.bulk :body => bulk_request - end - end - end -end diff --git a/templates/etc/mora.properties.mustache b/templates/etc/mora.properties.mustache index f092f802f..7cf43d238 100644 --- a/templates/etc/mora.properties.mustache +++ b/templates/etc/mora.properties.mustache @@ -1,4 +1,5 @@ http.server.host={{mora.host}} http.server.port={{mora.port}} mongod.api_umbrella.uri={{mongodb.url}} +mongod.api_umbrella.mode={{mongodb.read_preference}} mongod.api_umbrella.timeout={{mora.timeout}} diff --git a/templates/etc/nginx/frontend_defaults.conf.mustache b/templates/etc/nginx/frontend_defaults.conf.mustache index 4c14a5a6d..190b2112e 100644 --- a/templates/etc/nginx/frontend_defaults.conf.mustache +++ b/templates/etc/nginx/frontend_defaults.conf.mustache @@ -65,3 +65,20 @@ if ($banned_user_agent) { # Clear any headers used inside API Umbrella that should not be possible to set # by an external client. more_clear_input_headers X-Api-Umbrella-Allow-Authorization-Caching; + +{{#_development_env?}} + location /livereload.js { + proxy_pass http://127.0.0.1:{{ember_server.live_reload_port}}; + } + + location /livereload { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_pass http://127.0.0.1:{{ember_server.live_reload_port}}; + } + + location /ember-cli-live-reload.js { + proxy_pass http://127.0.0.1:{{ember_server.port}}; + } +{{/_development_env?}} diff --git a/templates/etc/nginx/gatekeeper.conf.mustache b/templates/etc/nginx/gatekeeper.conf.mustache index 909b67ff3..a2eef3524 100644 --- a/templates/etc/nginx/gatekeeper.conf.mustache +++ b/templates/etc/nginx/gatekeeper.conf.mustache @@ -27,3 +27,8 @@ header_filter_by_lua_file '{{_src_root_dir}}/src/api-umbrella/proxy/hooks/header log_by_lua_file '{{_src_root_dir}}/src/api-umbrella/proxy/hooks/log_initial_proxy.lua'; proxy_pass $api_umbrella_proxy_pass; + +proxy_redirect http://{{web.host}}:{{web.port}}/ /; +proxy_redirect http://$host:{{web.port}}/ /; +proxy_redirect http://{{static_site.host}}:{{static_site.port}}/ /; +proxy_redirect http://$host:{{static_site.port}}/ /; diff --git a/templates/etc/nginx/router.conf.mustache b/templates/etc/nginx/router.conf.mustache index b55943b9d..441047822 100644 --- a/templates/etc/nginx/router.conf.mustache +++ b/templates/etc/nginx/router.conf.mustache @@ -205,7 +205,6 @@ http { server { listen {{static_site.host}}:{{static_site.port}}; server_name _; - port_in_redirect off; root {{static_site.build_dir}}; } @@ -267,16 +266,37 @@ http { set $x_api_umbrella_request_id $http_x_api_umbrella_request_id; root {{web.dir}}/public; - # Serve non-static resources with Puma. - try_files $uri/index.html $uri.html $uri @app; - location @app { + {{^_development_env?}} + location /web-assets/ { + alias {{_embedded_root_dir}}/apps/core/current/build/dist/web-app-assets/web-assets/; + } + {{/_development_env?}} + + location ~ ^/admin/(login|logout|auth|stats|config|api_users|elasticsearch|i18n_detection) { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://puma; } - } - {{#_test_env?}} - include ./test_backends.conf; - {{/_test_env?}} + rewrite ^/admin$ /admin/ permanent; + location /admin/ { + {{^_development_env?}} + {{^_test_env?}} + alias {{_embedded_root_dir}}/apps/core/current/build/dist/admin-ui/; + {{/_test_env?}} + {{#_test_env?}} + alias {{_embedded_root_dir}}/apps/core/current/build/dist/admin-ui-dev/; + {{/_test_env?}} + {{/_development_env?}} + {{#_development_env?}} + proxy_pass http://127.0.0.1:{{ember_server.port}}; + {{/_development_env?}} + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_pass http://puma; + } + } } diff --git a/templates/etc/nginx/test_backends.conf.mustache b/templates/etc/nginx/test_backends.conf.mustache deleted file mode 100644 index e5987cfd2..000000000 --- a/templates/etc/nginx/test_backends.conf.mustache +++ /dev/null @@ -1,657 +0,0 @@ -lua_shared_dict test_data 32k; - -server { - listen 9444; - listen [::]:9444; - listen 127.0.0.1:9441; - listen [::1]:9441; - - set $x_api_umbrella_request_id $http_x_api_umbrella_request_id; - lua_use_default_type off; - default_type ""; - - gzip off; - - access_by_lua 'ngx.shared.test_data:set("backend_called", true)'; - - rewrite ^/backend-prefix/(.+)$ /$1 last; - - location /hello { - if ($request_method != POST) { - echo -n "Hello World"; - } - - if ($request_method = POST) { - echo -n "Goodbye"; - } - } - - location = /echo { - echo_read_request_body; - echo $request_body; - } - - location = /echo_delayed_chunked { - content_by_lua ' - local input = ngx.unescape_uri(ngx.var.arg_input) - for index = 1, #input do - local char = input:sub(index, index) - ngx.print(char) - ngx.flush() - ngx.sleep(math.random(5, 15) / 1000) - end - '; - } - - location = /restricted { - echo -n "Restricted Access"; - } - - location = /not/restricted { - echo -n "Not Restricted"; - } - - location = /utf8 { - more_set_headers "X-Example: tést"; - echo -n "Hellö Wörld"; - } - - location = /sleep { - echo_sleep 1; - echo -n "Sleepy head"; - } - - location = /sleep_timeout { - echo_sleep 1; - echo -n "Sleepy head"; - } - - location /auth/ { - access_by_lua ' - local username = ngx.var.remote_user - local password = ngx.var.remote_passwd - if username == "somebody" and password == "secret" then - return - elseif username == "anotheruser" and password == "anothersecret" then - return - else - ngx.header["WWW-Authenticate"] = [[Basic realm="Authorization Required"]] - ngx.status = ngx.HTTP_UNAUTHORIZED - ngx.print("Unauthorized") - ngx.exit(ngx.HTTP_OK) - end - '; - echo -n $remote_user; - } - - location /headers/ { - more_set_headers "X-Existing1: existing1"; - more_set_headers "x-existing2: existing2"; - more_set_headers "X-EXISTING3: existing3"; - echo -n "Hello World"; - } - - location = /redirect { - rewrite_by_lua 'ngx.redirect(ngx.unescape_uri(ngx.var.arg_to or "/hello"))'; - } - - location /info/ { - more_set_headers "X-Received-Method: $request_method"; - content_by_lua ' - local cjson = require "cjson"; - local raw_url = ngx.var.scheme .. "://" .. ngx.var.host .. ngx.var.request_uri - ngx.print(cjson.encode({ - method = ngx.var.request_method, - headers = ngx.req.get_headers(500), - local_interface_ip = ngx.var.server_addr, - raw_url = raw_url, - url = { - host = ngx.var.host, - hostname = ngx.var.hostname, - href = raw_url, - path = ngx.var.request_uri, - pathname = ngx.var.uri, - port = ngx.var.server_port, - protocol = ngx.var.scheme .. ":", - query = ngx.req.get_uri_args(500), - }, - basic_auth_username = ngx.var.remote_user, - basic_auth_password = ngx.var.remote_passwd, - })) - '; - } - - location = /upload { - content_by_lua ' - local cjson = require "cjson"; - local upload = require "resty.upload"; - - local upload_size = 0 - local chunk_size = 4096 - local form = upload:new(chunk_size) - while true do - local typ, res, err = form:read() - if typ == "body" then - upload_size = upload_size + #res - elseif typ == "eof" then - break - end - end - - ngx.print(cjson.encode({ - upload_size = upload_size, - })) - '; - } - - location = /chunked { - echo -n "hello"; - echo_flush; - echo_sleep 0.5; - echo -n "salutations"; - echo_flush; - echo_sleep 0.5; - echo -n "goodbye"; - } - - location = /receive_chunks { - # TODO - } - - location ~ ^/compressible/([0-9]+)$ { - set $size $1; - content_by_lua ' - ngx.header["Content-Type"] = ngx.unescape_uri(ngx.var.arg_content_type or "text/plain"); - ngx.header["Content-Length"] = ngx.var.size - ngx.print(string.rep("a", tonumber(ngx.var.size))) - '; - } - - location ~ ^/compressible-chunked/([0-9]+)/([0-9]+)$ { - set $chunks $1; - set $size $2; - content_by_lua ' - local size = tonumber(ngx.var.size) - ngx.header["Content-Type"] = ngx.unescape_uri(ngx.var.arg_content_type or "text/plain"); - ngx.sleep(0.05) - for index = 1, tonumber(ngx.var.chunks) do - ngx.print(string.rep("a", size)) - end - '; - } - - location ~ ^/compressible-delayed-chunked/([0-9]+)$ { - set $size $1; - content_by_lua ' - local size = tonumber(ngx.var.size) - ngx.header["Content-Type"] = ngx.unescape_uri(ngx.var.arg_content_type or "text/plain"); - - ngx.print(string.rep("a", size)) - ngx.flush() - ngx.sleep(0.5) - - ngx.print(string.rep("a", size)) - ngx.flush() - ngx.sleep(0.5) - - ngx.print(string.rep("a", size)) - '; - } - - location = /compressible-pre-gzip { - gzip on; - echo -n "Hello Small World"; - } - - location ~ ^/delay/([0-9]+)$ { - set $delay $1; - content_by_lua ' - ngx.sleep(tonumber(ngx.var.delay) / 1000) - ngx.print("done") - '; - } - - location ~ ^/delays/([0-9]+)/([0-9]+)$ { - set $delay1 $1; - set $delay2 $2; - content_by_lua ' - local delay1 = tonumber(ngx.var.delay1) / 1000 - local delay2 = tonumber(ngx.var.delay2) / 1000 - - ngx.sleep(delay1) - ngx.print("first") - ngx.flush() - - ngx.sleep(delay2 - delay1) - ngx.print("done") - ngx.flush() - '; - } - - location = /timeout { - access_by_lua ' - local key = "backend_call_count:" .. string.lower(ngx.var.request_method) .. "-timeout" - return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) - '; - - echo_sleep 20; - echo -n "done"; - } - - location = /between-varnish-timeout { - access_by_lua ' - local key = "backend_call_count:post-between-varnish-timeout" - return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) - '; - - echo_sleep 12.5; - echo -n "done"; - } - - location /cacheable-but-not/ { - set_secure_random_alphanum $random 50; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location ~ ^/cacheable-thundering-herd/(.+)$ { - set $unique_id $1; - access_by_lua ' - local key = "backend_call_count:" .. ngx.var.unique_id - return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) - '; - - echo_sleep 1; - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location ~ ^/cacheable-thundering-herd-public/(.+)$ { - set $unique_id $1; - access_by_lua ' - local key = "backend_call_count:" .. ngx.var.unique_id - return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) - '; - - echo_sleep 1; - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: public, max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location ~ ^/cacheable-thundering-herd-private/(.+)$ { - set $unique_id $1; - access_by_lua ' - local key = "backend_call_count:" .. ngx.var.unique_id - return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) - '; - - echo_sleep 1; - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: private, max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location ~ ^/cacheable-but-no-explicit-cache-thundering-herd/(.+)$ { - set $unique_id $1; - access_by_lua ' - local key = "backend_call_count:" .. ngx.var.unique_id - return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) - '; - - echo_sleep 1; - set_secure_random_alphanum $random 50; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location ~ ^/cacheable-but-cache-forbidden-thundering-herd/(.+)$ { - set $unique_id $1; - access_by_lua ' - local key = "backend_call_count:" .. ngx.var.unique_id - return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) - '; - - echo_sleep 1; - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=0, private, must-revalidate"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-cache-control-max-age/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-cache-control-s-maxage/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: s-maxage=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-cache-control-case-insensitive/ { - set_secure_random_alphanum $random 50; - more_set_headers "CAcHE-cONTROL: max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-expires/ { - set_secure_random_alphanum $random 50; - more_set_headers "X-Unique-Output: $random"; - expires 60; - echo $random; - } - - location /cacheable-expires-0/ { - set_secure_random_alphanum $random 50; - more_set_headers "Expires: 0"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-expires-past/ { - set_secure_random_alphanum $random 50; - more_set_headers "Expires: Sat, 05 Sep 2015 17:58:16 GMT"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-set-cookie/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Set-Cookie: foo=bar"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-www-authenticate/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "WWW-Authenticate: Basic"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-surrogate-control-max-age/ { - set_secure_random_alphanum $random 50; - more_set_headers "Surrogate-Control: max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-surrogate-control-case-insensitive/ { - set_secure_random_alphanum $random 50; - more_set_headers "SURrOGATE-CONtROL: max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-surrogate-control-and-cache-control/ { - set_secure_random_alphanum $random 50; - more_set_headers "Surrogate-Control: max-age=60"; - more_set_headers "Cache-Control: max-age=0, private, must-revalidate"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-dynamic/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-compressible/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Content-Type: text/plain"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip - } - - location /cacheable-pre-gzip/ { - gzip on; - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Content-Type: text/plain"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip - } - - location /cacheable-vary-accept-encoding/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Content-Type: text/plain"; - more_set_headers "Vary: Accept-Encoding"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip - } - - location /cacheable-pre-gzip-multiple-vary/ { - gzip on; - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Content-Type: text/plain"; - more_set_headers "Vary: X-Foo,Accept-Encoding,Accept"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip - } - - location /cacheable-vary-accept-encoding-multiple/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Content-Type: text/plain"; - more_set_headers "Vary: X-Foo,Accept-Encoding,Accept"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip - } - - location /cacheable-vary-x-custom/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Vary: X-Custom"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-vary-accept-encoding-accept-separate/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Vary: Accept-Encoding"; - more_set_headers "Vary: Accept"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-multiple-vary/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Content-Type: text/plain"; - more_set_headers "Vary: X-Foo,Accept-Language,Accept"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - } - - location /cacheable-multiple-vary-with-accept-encoding/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Content-Type: text/plain"; - more_set_headers "Vary: X-Foo,Accept-Language,Accept-Encoding,Accept"; - more_set_headers "X-Unique-Output: $random"; - echo $random; - echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip - } - - location /cacheable-backend-reports-cached/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Age: 3"; - more_set_headers "X-Cache: HIT"; - echo $random; - } - - location /cacheable-backend-reports-not-cached/ { - set_secure_random_alphanum $random 50; - more_set_headers "Cache-Control: max-age=60"; - more_set_headers "Age: 0"; - more_set_headers "X-Cache: BACKEND-MISS"; - echo $random; - } - - location /cacheable-backend-port/ { - more_set_headers "Cache-Control: max-age=60"; - set_secure_random_alphanum $random 50; - more_set_headers "X-Unique-Output: $random"; - echo -n $server_port; - } - - location /cacheable-backend-host/ { - more_set_headers "Cache-Control: max-age=60"; - set_secure_random_alphanum $random 50; - more_set_headers "X-Unique-Output: $random"; - echo -n $host; - } - - location /via-header/ { - more_set_headers "Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)"; - echo -n "hello"; - } - - location /logging-example/ { - more_set_headers "Age: 20"; - more_set_headers "Content-Type: text/plain; charset=utf-8"; - more_set_headers "Content-Length: 5"; - expires 60; - echo -n "hello"; - } - - location /logging-multiple-request-headers/ { - content_by_lua_block { - local cjson = require "cjson" - local raw_header = ngx.req.raw_header(true) - local _, num_matches = ngx.re.gsub(raw_header, "^" .. ngx.var.arg_header .. ": ", "", "im") - ngx.print(cjson.encode({ - header_occurrences_received = num_matches, - })) - } - } - - location /logging-multiple-response-headers/ { - content_by_lua_block { - ngx.header[ngx.var.arg_header] = { "11", "22" } - ngx.print("OK") - } - } - - location /logging-long-response-headers/ { - more_set_headers "Content-Encoding: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; - more_set_headers "Content-Type: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; - more_set_headers "Server: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; - more_set_headers "Transfer-Encoding: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; - echo $random; - } - - location = / { - echo -n "Test Home Page"; - } - - location / { - echo_status 404; - more_set_headers "Content-Type: text/html"; - echo -n "Test 404 Not Found"; - } -} - -server { - listen 127.0.0.1:9443; - listen [::1]:9443; - - location = / { - echo -n "Test Website Home Page"; - } - - location / { - echo_status 404; - more_set_headers "Content-Type: text/html"; - echo -n "Test Website 404 Not Found"; - } -} - -server { - listen 127.0.0.1:9442; - listen [::1]:9442; - - set $x_api_umbrella_request_id $http_x_api_umbrella_request_id; - - location = /reset_backend_called { - content_by_lua 'ngx.shared.test_data:set("backend_called", false)'; - } - - location = /backend_called { - content_by_lua 'ngx.print(ngx.shared.test_data:get("backend_called"))'; - } - - location = /backend_call_count { - content_by_lua 'ngx.print(ngx.shared.test_data:get("backend_call_count:" .. ngx.var.arg_id) or 0)'; - } -} - -server { - listen 127.0.0.1:9448 default_server ssl; - listen [::1]:9448 default_server ssl; - server_name _; - ssl on; - ssl_certificate {{_src_root_dir}}/test/config/ssl_test.crt; - ssl_certificate_key {{_src_root_dir}}/test/config/ssl_test.key; - - # As the default server terminate the SSL connection. This allows us to test - # for situations where the backend server requires SNI. - ssl_certificate_by_lua_block { - local ssl = require "ngx.ssl" - local ok, err = ssl.clear_certs() - if not ok then - ngx.log(ngx.ERR, "failed to clear existing (fallback) certificates") - return ngx.exit(ngx.ERROR) - end - } -} - -server { - listen 127.0.0.1:9448 ssl; - listen [::1]:9448 ssl; - server_name sni1.foo.ooga; - ssl on; - ssl_certificate {{_src_root_dir}}/test/config/ssl_test.crt; - ssl_certificate_key {{_src_root_dir}}/test/config/ssl_test.key; - - location / { - echo -n "SNI1"; - } -} - -server { - listen 127.0.0.1:9448 ssl; - listen [::1]:9448 ssl; - server_name sni2.foo.ooga; - ssl on; - ssl_certificate {{_src_root_dir}}/test/config/ssl_test.crt; - ssl_certificate_key {{_src_root_dir}}/test/config/ssl_test.key; - - location / { - echo -n "SNI2"; - } -} diff --git a/templates/etc/perp/dev-env-ember-server/rc.env.mustache b/templates/etc/perp/dev-env-ember-server/rc.env.mustache new file mode 100644 index 000000000..a3bb0ef68 --- /dev/null +++ b/templates/etc/perp/dev-env-ember-server/rc.env.mustache @@ -0,0 +1,4 @@ +XDG_DATA_HOME={{tmp_dir}}/node-xdg/data +XDG_CONFIG_HOME={{tmp_dir}}/node-xdg/config +XDG_CACHE_HOME={{tmp_dir}}/node-xdg/cache +EMBER_CLI_INJECT_LIVE_RELOAD_PORT={{https_port}} diff --git a/templates/etc/perp/dev-env-ember-server/rc.log b/templates/etc/perp/dev-env-ember-server/rc.log new file mode 100755 index 000000000..089baa65a --- /dev/null +++ b/templates/etc/perp/dev-env-ember-server/rc.log @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec ../rc.log "$@" diff --git a/templates/etc/perp/dev-env-ember-server/rc.main.mustache b/templates/etc/perp/dev-env-ember-server/rc.main.mustache new file mode 100755 index 000000000..53be1c839 --- /dev/null +++ b/templates/etc/perp/dev-env-ember-server/rc.main.mustache @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# Redirect stderr to stdout +exec 2>&1 + + +if [ "${1}" = "start" ]; then + echo "starting ${2}..." + api_umbrella_user="{{user}}" + + run_args=("-e" "rc.env" "-c" "/vagrant-admin-ui") + if [ -n "$api_umbrella_user" ]; then + run_args+=("-u" "$api_umbrella_user") + fi + + PATH="{{_dev_env_install_dir}}/sbin:{{_dev_env_install_dir}}/bin:$PATH" + exec env runtool "${run_args[@]}" \ + ./node_modules/.bin/ember server \ + --port "{{ember_server.port}}" \ + --live-reload-port "{{ember_server.live_reload_port}}" +fi + +exit 0 diff --git a/templates/etc/perp/test-env-mailhog/rc.log b/templates/etc/perp/test-env-mailhog/rc.log new file mode 100755 index 000000000..089baa65a --- /dev/null +++ b/templates/etc/perp/test-env-mailhog/rc.log @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec ../rc.log "$@" diff --git a/templates/etc/perp/test-env-mailhog/rc.main.mustache b/templates/etc/perp/test-env-mailhog/rc.main.mustache new file mode 100755 index 000000000..b1f427118 --- /dev/null +++ b/templates/etc/perp/test-env-mailhog/rc.main.mustache @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Redirect stderr to stdout +exec 2>&1 + +if [ "${1}" = "start" ]; then + echo "starting ${2}..." + api_umbrella_user="{{user}}" + + PATH="{{_test_env_install_dir}}/sbin:{{_test_env_install_dir}}/bin:$PATH" + run_args=() + if [ -n "$api_umbrella_user" ]; then + run_args+=("-u" "$api_umbrella_user") + fi + + exec runtool "${run_args[@]}" mailhog \ + -smtp-bind-addr "{{mailhog.smtp_bind_addr}}" \ + -api-bind-addr "{{mailhog.api_bind_addr}}" \ + -ui-bind-addr "{{mailhog.ui_bind_addr}}" +fi + +exit 0 diff --git a/templates/etc/perp/test-env-nginx/rc.env.mustache b/templates/etc/perp/test-env-nginx/rc.env.mustache new file mode 100644 index 000000000..ae402242e --- /dev/null +++ b/templates/etc/perp/test-env-nginx/rc.env.mustache @@ -0,0 +1 @@ +LD_LIBRARY_PATH={{_embedded_root_dir}}/lib diff --git a/templates/etc/perp/test-env-nginx/rc.log b/templates/etc/perp/test-env-nginx/rc.log new file mode 100755 index 000000000..089baa65a --- /dev/null +++ b/templates/etc/perp/test-env-nginx/rc.log @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec ../rc.log "$@" diff --git a/templates/etc/perp/test-env-nginx/rc.main.mustache b/templates/etc/perp/test-env-nginx/rc.main.mustache new file mode 100755 index 000000000..8342e38a1 --- /dev/null +++ b/templates/etc/perp/test-env-nginx/rc.main.mustache @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Redirect stderr to stdout +exec 2>&1 + +if [ "${1}" = "start" ]; then + echo "starting ${2}..." + + run_args=("-e" "rc.env" "-c" "{{_src_root_dir}}") + exec runtool "${run_args[@]}" nginx -p "{{_src_root_dir}}/" -c "{{etc_dir}}/test-env/nginx/nginx.conf" +fi + +exit 0 diff --git a/templates/etc/perp/web-delayed-job/rc.env.mustache b/templates/etc/perp/web-delayed-job/rc.env.mustache index f6a38c628..b48a237d5 100644 --- a/templates/etc/perp/web-delayed-job/rc.env.mustache +++ b/templates/etc/perp/web-delayed-job/rc.env.mustache @@ -1,5 +1,5 @@ RAILS_ENV={{app_env}} API_UMBRELLA_RUNTIME_CONFIG={{_api_umbrella_config_runtime_file}} -{{#_test_env?}} -FULL_STACK_TEST=true -{{/_test_env?}} +RAILS_LOG_TO_STDOUT=true +RAILS_TMP_PATH={{tmp_dir}}/web-app +RAILS_PUBLIC_PATH={{_embedded_root_dir}}/apps/core/current/build/dist/web-app-assets diff --git a/templates/etc/perp/web-delayed-job/rc.main.mustache b/templates/etc/perp/web-delayed-job/rc.main.mustache index 6b713bcd7..60b9aaa72 100755 --- a/templates/etc/perp/web-delayed-job/rc.main.mustache +++ b/templates/etc/perp/web-delayed-job/rc.main.mustache @@ -12,7 +12,7 @@ if [ "${1}" = "start" ]; then run_args+=("-u" "$api_umbrella_user") fi - exec runtool "${run_args[@]}" bundle exec ./script/delayed_job --pid-dir="{{run_dir}}" --queues=mailers run + exec runtool "${run_args[@]}" bundle exec ./bin/delayed_job --pid-dir="{{run_dir}}" run fi exit 0 diff --git a/templates/etc/perp/web-puma/rc.env.mustache b/templates/etc/perp/web-puma/rc.env.mustache index 6d356448c..b48a237d5 100644 --- a/templates/etc/perp/web-puma/rc.env.mustache +++ b/templates/etc/perp/web-puma/rc.env.mustache @@ -1,4 +1,5 @@ +RAILS_ENV={{app_env}} API_UMBRELLA_RUNTIME_CONFIG={{_api_umbrella_config_runtime_file}} -{{#_test_env?}} -FULL_STACK_TEST=true -{{/_test_env?}} +RAILS_LOG_TO_STDOUT=true +RAILS_TMP_PATH={{tmp_dir}}/web-app +RAILS_PUBLIC_PATH={{_embedded_root_dir}}/apps/core/current/build/dist/web-app-assets diff --git a/templates/etc/rsyslog.conf.mustache b/templates/etc/rsyslog.conf.mustache index f1b56fe9f..bfe05c41f 100644 --- a/templates/etc/rsyslog.conf.mustache +++ b/templates/etc/rsyslog.conf.mustache @@ -85,14 +85,27 @@ local0.info action( dynSearchIndex="on" searchType="log" template="elasticsearch-json-record" + # Enable bulk indexing, so batches of records are sent as a single HTTP + # request. bulkmode="on" bulkId="elasticsearch-id" dynBulkId="on" + # Require indexing by all replica shards. asyncrepl="off" + # For the in-memory queue, use a linked-list (so the memory doesn't have to + # be pre-allocated based on a fixed size). queue.type="LinkedList" + # Set a filename, so the queue is disk assisted. This allows for offloading + # the data from the memory queue to disk if the queue becomes bigger than + # expected. queue.filename="queue-elasticsearch" + # Persist data to disk on this interval (in seconds). We're okay with some + # loss in the event of unexpected failures. queue.checkpointinterval="10" + # Persist data to disk on graceful shutdowns. queue.saveonshutdown="on" + # Set thresholds for when the memory queue is too big and should use the + # disk. queue.highwatermark="10000" queue.lowwatermark="2000" action.resumeRetryCount="-1" diff --git a/templates/etc/test-env/nginx/apis.conf.mustache b/templates/etc/test-env/nginx/apis.conf.mustache new file mode 100644 index 000000000..50c95d123 --- /dev/null +++ b/templates/etc/test-env/nginx/apis.conf.mustache @@ -0,0 +1,720 @@ +lua_shared_dict test_data 32k; + +server { + listen 9444; + listen [::]:9444; + listen 127.0.0.1:9441; + listen [::1]:9441; + + set $x_api_umbrella_request_id $http_x_api_umbrella_request_id; + lua_use_default_type off; + default_type ""; + + gzip off; + + access_by_lua 'ngx.shared.test_data:set("backend_called", true)'; + + rewrite ^/backend-prefix/(.+)$ /$1 last; + + location /hello { + if ($request_method != POST) { + echo -n "Hello World"; + } + + if ($request_method = POST) { + echo -n "Goodbye"; + } + } + + location = /echo { + echo_read_request_body; + echo $request_body; + } + + location = /echo_delayed_chunked { + content_by_lua ' + local input = ngx.unescape_uri(ngx.var.arg_input) + for index = 1, #input do + local char = input:sub(index, index) + ngx.print(char) + ngx.flush() + ngx.sleep(math.random(5, 15) / 1000) + end + '; + } + + location = /restricted { + echo -n "Restricted Access"; + } + + location = /not/restricted { + echo -n "Not Restricted"; + } + + location = /utf8 { + more_set_headers "X-Example: tést"; + echo -n "Hellö Wörld"; + } + + location = /sleep { + echo_sleep 1; + echo -n "Sleepy head"; + } + + location = /sleep_timeout { + echo_sleep 1; + echo -n "Sleepy head"; + } + + location /auth/ { + access_by_lua ' + local username = ngx.var.remote_user + local password = ngx.var.remote_passwd + if username == "somebody" and password == "secret" then + return + elseif username == "anotheruser" and password == "anothersecret" then + return + else + ngx.header["WWW-Authenticate"] = [[Basic realm="Authorization Required"]] + ngx.status = ngx.HTTP_UNAUTHORIZED + ngx.print("Unauthorized") + ngx.exit(ngx.HTTP_OK) + end + '; + echo -n $remote_user; + } + + location /headers/ { + more_set_headers "X-Existing1: existing1"; + more_set_headers "x-existing2: existing2"; + more_set_headers "X-EXISTING3: existing3"; + echo -n "Hello World"; + } + + location = /redirect { + rewrite_by_lua 'ngx.redirect(ngx.unescape_uri(ngx.var.arg_to or "/hello"))'; + } + + location /info/ { + more_set_headers "X-Received-Method: $request_method"; + content_by_lua ' + local cjson = require "cjson"; + local raw_url = ngx.var.scheme .. "://" .. ngx.var.host .. ngx.var.request_uri + ngx.header["Content-Type"] = "application/json" + ngx.print(cjson.encode({ + method = ngx.var.request_method, + headers = ngx.req.get_headers(500), + local_interface_ip = ngx.var.server_addr, + raw_url = raw_url, + url = { + host = ngx.var.host, + hostname = ngx.var.hostname, + href = raw_url, + path = ngx.var.request_uri, + pathname = ngx.var.uri, + port = ngx.var.server_port, + protocol = ngx.var.scheme .. ":", + query = ngx.req.get_uri_args(500), + }, + basic_auth_username = ngx.var.remote_user, + basic_auth_password = ngx.var.remote_passwd, + request_length = tonumber(ngx.var.request_length), + })) + '; + } + + location /connection-stats/ { + content_by_lua_block { + local cjson = require "cjson"; + ngx.sleep(0.3) + ngx.header["Content-Type"] = "application/json" + ngx.say(cjson.encode({ + connection = tonumber(ngx.var.connection), + connection_requests = tonumber(ngx.var.connection_requests), + connections_active = tonumber(ngx.var.connections_active), + connections_reading = tonumber(ngx.var.connections_reading), + connections_waiting = tonumber(ngx.var.connections_waiting), + connections_writing = tonumber(ngx.var.connections_writing), + })) + } + } + + location = /upload { + content_by_lua ' + local cjson = require "cjson"; + local upload = require "resty.upload"; + + local upload_size = 0 + local chunk_size = 4096 + local form = upload:new(chunk_size) + while true do + local typ, res, err = form:read() + if typ == "body" then + upload_size = upload_size + #res + elseif typ == "eof" then + break + end + end + + ngx.header["Content-Type"] = "application/json" + ngx.print(cjson.encode({ + upload_size = upload_size, + })) + '; + } + + location = /chunked { + echo -n "hello"; + echo_flush; + echo_sleep 0.5; + echo -n "salutations"; + echo_flush; + echo_sleep 0.5; + echo -n "goodbye"; + } + + location = /receive_chunks { + # TODO + } + + location ~ ^/compressible/([0-9]+)$ { + set $size $1; + content_by_lua ' + ngx.header["Content-Type"] = ngx.unescape_uri(ngx.var.arg_content_type or "text/plain"); + ngx.header["Content-Length"] = ngx.var.size + ngx.print(string.rep("a", tonumber(ngx.var.size))) + '; + } + + location ~ ^/compressible-chunked/([0-9]+)/([0-9]+)$ { + set $chunks $1; + set $size $2; + content_by_lua ' + local size = tonumber(ngx.var.size) + ngx.header["Content-Type"] = ngx.unescape_uri(ngx.var.arg_content_type or "text/plain"); + ngx.sleep(0.05) + for index = 1, tonumber(ngx.var.chunks) do + ngx.print(string.rep("a", size)) + end + '; + } + + location ~ ^/compressible-delayed-chunked/([0-9]+)$ { + set $size $1; + content_by_lua ' + local size = tonumber(ngx.var.size) + ngx.header["Content-Type"] = ngx.unescape_uri(ngx.var.arg_content_type or "text/plain"); + + ngx.print(string.rep("a", size)) + ngx.flush() + ngx.sleep(0.5) + + ngx.print(string.rep("a", size)) + ngx.flush() + ngx.sleep(0.5) + + ngx.print(string.rep("a", size)) + '; + } + + location = /compressible-pre-gzip { + gzip on; + echo -n "Hello Small World"; + } + + location ~ ^/delay/([0-9]+)$ { + set $delay $1; + content_by_lua ' + ngx.sleep(tonumber(ngx.var.delay) / 1000) + ngx.print("done") + '; + } + + location ~ ^/delay-sec/([0-9.]+)$ { + set $delay $1; + content_by_lua ' + ngx.sleep(tonumber(ngx.var.delay)) + ngx.print("done") + '; + } + + location ~ ^/delays/([0-9]+)/([0-9]+)$ { + set $delay1 $1; + set $delay2 $2; + content_by_lua ' + local delay1 = tonumber(ngx.var.delay1) / 1000 + local delay2 = tonumber(ngx.var.delay2) / 1000 + + ngx.sleep(delay1) + ngx.print("first") + ngx.flush() + + ngx.sleep(delay2 - delay1) + ngx.print("done") + ngx.flush() + '; + } + + location ~ ^/delays-sec/([0-9]+)/([0-9]+)$ { + set $delay1 $1; + set $delay2 $2; + content_by_lua ' + local delay1 = tonumber(ngx.var.delay1) + local delay2 = tonumber(ngx.var.delay2) + + ngx.sleep(delay1) + ngx.print("first") + ngx.flush() + + ngx.sleep(delay2 - delay1) + ngx.print("done") + ngx.flush() + '; + } + + location = /timeout { + access_by_lua ' + local key = "backend_call_count:" .. string.lower(ngx.var.request_method) .. "-timeout" + return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) + '; + + echo_sleep 20; + echo -n "done"; + } + + location = /between-varnish-timeout { + access_by_lua ' + local key = "backend_call_count:post-between-varnish-timeout" + return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) + '; + + echo_sleep 12.5; + echo -n "done"; + } + + location /cacheable-but-not/ { + set_secure_random_alphanum $random 50; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location ~ ^/cacheable-thundering-herd/(.+)$ { + set $unique_id $1; + access_by_lua ' + local key = "backend_call_count:" .. ngx.var.unique_id + return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) + '; + + echo_sleep 1; + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location ~ ^/cacheable-thundering-herd-public/(.+)$ { + set $unique_id $1; + access_by_lua ' + local key = "backend_call_count:" .. ngx.var.unique_id + return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) + '; + + echo_sleep 1; + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: public, max-age=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location ~ ^/cacheable-thundering-herd-private/(.+)$ { + set $unique_id $1; + access_by_lua ' + local key = "backend_call_count:" .. ngx.var.unique_id + return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) + '; + + echo_sleep 1; + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: private, max-age=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location ~ ^/cacheable-but-no-explicit-cache-thundering-herd/(.+)$ { + set $unique_id $1; + access_by_lua ' + local key = "backend_call_count:" .. ngx.var.unique_id + return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) + '; + + echo_sleep 1; + set_secure_random_alphanum $random 50; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location ~ ^/cacheable-but-cache-forbidden-thundering-herd/(.+)$ { + set $unique_id $1; + access_by_lua ' + local key = "backend_call_count:" .. ngx.var.unique_id + return ngx.shared.test_data:incr(key, 1) or ngx.shared.test_data:set(key, 1) + '; + + echo_sleep 1; + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=0, private, must-revalidate"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-cache-control-max-age/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "X-Unique-Output: $random"; + more_set_headers "X-Received-Method: $request_method"; + echo $random; + } + + location /cacheable-cache-control-s-maxage/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: s-maxage=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-cache-control-case-insensitive/ { + set_secure_random_alphanum $random 50; + more_set_headers "CAcHE-cONTROL: max-age=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-expires/ { + set_secure_random_alphanum $random 50; + more_set_headers "X-Unique-Output: $random"; + expires 60; + echo $random; + } + + location /cacheable-expires-0/ { + set_secure_random_alphanum $random 50; + more_set_headers "Expires: 0"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-expires-past/ { + set_secure_random_alphanum $random 50; + more_set_headers "Expires: Sat, 05 Sep 2015 17:58:16 GMT"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-set-cookie/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Set-Cookie: foo=bar"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-www-authenticate/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "WWW-Authenticate: Basic"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-surrogate-control-max-age/ { + set_secure_random_alphanum $random 50; + more_set_headers "Surrogate-Control: max-age=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-surrogate-control-case-insensitive/ { + set_secure_random_alphanum $random 50; + more_set_headers "SURrOGATE-CONtROL: max-age=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-surrogate-control-and-cache-control/ { + set_secure_random_alphanum $random 50; + more_set_headers "Surrogate-Control: max-age=60"; + more_set_headers "Cache-Control: max-age=0, private, must-revalidate"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-dynamic/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-compressible/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Content-Type: text/plain"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip + } + + location /cacheable-pre-gzip/ { + gzip on; + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Content-Type: text/plain"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip + } + + location /cacheable-vary-accept-encoding/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Content-Type: text/plain"; + more_set_headers "Vary: Accept-Encoding"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip + } + + location /cacheable-pre-gzip-multiple-vary/ { + gzip on; + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Content-Type: text/plain"; + more_set_headers "Vary: X-Foo,Accept-Encoding,Accept"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip + } + + location /cacheable-vary-accept-encoding-multiple/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Content-Type: text/plain"; + more_set_headers "Vary: X-Foo,Accept-Encoding,Accept"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip + } + + location /cacheable-vary-x-custom/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Vary: X-Custom"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-vary-accept-encoding-accept-separate/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Vary: Accept-Encoding"; + more_set_headers "Vary: Accept"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-multiple-vary/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Content-Type: text/plain"; + more_set_headers "Vary: X-Foo,Accept-Language,Accept"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + } + + location /cacheable-multiple-vary-with-accept-encoding/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Content-Type: text/plain"; + more_set_headers "Vary: X-Foo,Accept-Language,Accept-Encoding,Accept"; + more_set_headers "X-Unique-Output: $random"; + echo $random; + echo_duplicate 1500 "a"; # 1500 bytes - long enough to allow gzip + } + + location /cacheable-backend-reports-cached/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Age: 3"; + more_set_headers "X-Cache: HIT"; + echo $random; + } + + location /cacheable-backend-reports-not-cached/ { + set_secure_random_alphanum $random 50; + more_set_headers "Cache-Control: max-age=60"; + more_set_headers "Age: 0"; + more_set_headers "X-Cache: BACKEND-MISS"; + echo $random; + } + + location /cacheable-backend-port/ { + more_set_headers "Cache-Control: max-age=60"; + set_secure_random_alphanum $random 50; + more_set_headers "X-Unique-Output: $random"; + echo -n $server_port; + } + + location /cacheable-backend-host/ { + more_set_headers "Cache-Control: max-age=60"; + set_secure_random_alphanum $random 50; + more_set_headers "X-Unique-Output: $random"; + echo -n $host; + } + + location /via-header/ { + more_set_headers "Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)"; + echo -n "hello"; + } + + location /logging-example/ { + more_set_headers "Age: 20"; + more_set_headers "Content-Type: text/plain; charset=utf-8"; + more_set_headers "Content-Length: 5"; + expires 60; + echo -n "hello"; + } + + location /logging-multiple-request-headers/ { + content_by_lua_block { + local cjson = require "cjson" + local header = ngx.var.arg_header + local raw_header = ngx.req.raw_header(true) + local _, num_matches = ngx.re.gsub(raw_header, "^" .. header .. ": ", "", "im") + ngx.header["Content-Type"] = "application/json" + ngx.print(cjson.encode({ + header_occurrences_received = num_matches, + header_value = ngx.req.get_headers()[header], + })) + } + } + + location /logging-multiple-response-headers/ { + content_by_lua_block { + ngx.header[ngx.var.arg_header] = { "11", "22" } + ngx.print("OK") + } + } + + location /logging-long-response-headers/ { + more_set_headers "Content-Encoding: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + more_set_headers "Content-Type: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + more_set_headers "Server: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + more_set_headers "Transfer-Encoding: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + echo $random; + } + + location = / { + echo -n "Test Home Page"; + } + + location / { + echo_status 404; + more_set_headers "Content-Type: text/html"; + echo -n "Test 404 Not Found"; + } +} + +server { + listen 127.0.0.1:9443; + listen [::1]:9443; + + location = / { + echo -n "Test Website Home Page"; + } + + location / { + echo_status 404; + more_set_headers "Content-Type: text/plain"; + echo -n "Test Website 404 Not Found"; + } +} + +server { + listen 127.0.0.1:9440; + listen [::1]:9440; + + location = / { + echo -n "Test Default Website Home Page"; + } + + location / { + echo_status 404; + more_set_headers "Content-Type: text/plain"; + echo -n "Test Default Website 404 Not Found"; + } +} + +server { + listen 127.0.0.1:9442; + listen [::1]:9442; + + set $x_api_umbrella_request_id $http_x_api_umbrella_request_id; + + location = /reset_backend_called { + content_by_lua 'ngx.shared.test_data:set("backend_called", false)'; + } + + location = /backend_called { + content_by_lua 'ngx.print(ngx.shared.test_data:get("backend_called"))'; + } + + location = /backend_call_count { + content_by_lua 'ngx.print(ngx.shared.test_data:get("backend_call_count:" .. ngx.var.arg_id) or 0)'; + } +} + +server { + listen 127.0.0.1:9448 default_server ssl; + listen [::1]:9448 default_server ssl; + server_name _; + ssl on; + ssl_certificate {{_src_root_dir}}/test/config/ssl_test.crt; + ssl_certificate_key {{_src_root_dir}}/test/config/ssl_test.key; + + # As the default server terminate the SSL connection. This allows us to test + # for situations where the backend server requires SNI. + ssl_certificate_by_lua_block { + local ssl = require "ngx.ssl" + local ok, err = ssl.clear_certs() + if not ok then + ngx.log(ngx.ERR, "failed to clear existing (fallback) certificates") + return ngx.exit(ngx.ERROR) + end + } +} + +server { + listen 127.0.0.1:9448 ssl; + listen [::1]:9448 ssl; + server_name sni1.foo.ooga; + ssl on; + ssl_certificate {{_src_root_dir}}/test/config/ssl_test.crt; + ssl_certificate_key {{_src_root_dir}}/test/config/ssl_test.key; + + location / { + echo -n "SNI1"; + } +} + +server { + listen 127.0.0.1:9448 ssl; + listen [::1]:9448 ssl; + server_name sni2.foo.ooga; + ssl on; + ssl_certificate {{_src_root_dir}}/test/config/ssl_test.crt; + ssl_certificate_key {{_src_root_dir}}/test/config/ssl_test.key; + + location / { + echo -n "SNI2"; + } +} diff --git a/templates/etc/test-env/nginx/nginx.conf.mustache b/templates/etc/test-env/nginx/nginx.conf.mustache new file mode 100644 index 000000000..64c6fea31 --- /dev/null +++ b/templates/etc/test-env/nginx/nginx.conf.mustache @@ -0,0 +1,38 @@ +daemon off; + +pid {{run_dir}}/test-env-nginx.pid; + +events { + worker_connections 1024; +} + +error_log stderr; + +pcre_jit on; + +http { + access_log {{log_dir}}/test-env-nginx/access.log combined; + + client_body_temp_path {{tmp_dir}}/nginx-client_body_temp; + proxy_temp_path {{tmp_dir}}/nginx-proxy_temp; + fastcgi_temp_path {{tmp_dir}}/nginx-fastcgi_temp; + uwsgi_temp_path {{tmp_dir}}/nginx-uwsgi_temp; + scgi_temp_path {{tmp_dir}}/nginx-scgi_temp; + server_tokens off; + + lua_package_path '{{_package_path}}'; + lua_package_cpath '{{_package_cpath}}'; + + gzip on; + gzip_comp_level 2; + gzip_disable msie6; + gzip_min_length 1000; + gzip_proxied any; + gzip_types application/atom+xml application/javascript application/json application/rss+xml application/x-javascript application/xml text/css text/csv text/javascript text/plain text/xml; + gzip_vary on; + + # Don't restrict file upload size. + client_max_body_size 0; + + include ./apis.conf; +} diff --git a/templates/etc/test-env/unbound/active_test.conf b/templates/etc/test-env/unbound/active_test.conf new file mode 100644 index 000000000..d0f771299 --- /dev/null +++ b/templates/etc/test-env/unbound/active_test.conf @@ -0,0 +1,2 @@ +# Empty initial file needed for startup (so unbound.conf's "include" of this +# file doesn't fail). Will be overwritten by individual tests. diff --git a/templates/etc/trafficserver/logs_xml.config b/templates/etc/trafficserver/logs_xml.config index 0f026b519..9c0a72ae0 100644 --- a/templates/etc/trafficserver/logs_xml.config +++ b/templates/etc/trafficserver/logs_xml.config @@ -1,10 +1,10 @@ - - % % %/% % % % % %/% %" /> + + - % [%] \"%\" % % %<{X-Api-Umbrella-Request-ID}cqh> sssc=% sscl=% cqbl=% pqbl=% cqhl=% pshl=% pqhl=% sshl=% ttms=% stms=% phr=% cfsc=% pfsc=% crc=% chm=% cwr=% cwtr=%"/> - + diff --git a/test/.jshintrc b/test/.jshintrc deleted file mode 100644 index 43d73d3c1..000000000 --- a/test/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "node": true, - "browser": false, - "esnext": true, - "bitwise": true, - "camelcase": false, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "sub": true, - "undef": true, - "unused": true, - "strict": true, - "trailing": true, - "smarttabs": true, - "globals": { - "after": true, - "afterEach": true, - "before": true, - "beforeEach": true, - "describe": true, - "gatekeeper": true, - "it": true, - "shared": true, - "should": true, - "redisClient": true, - "xdescribe": true, - "xit": true - } -} diff --git a/test/.rubocop.yml b/test/.rubocop.yml new file mode 100644 index 000000000..b1f3012bb --- /dev/null +++ b/test/.rubocop.yml @@ -0,0 +1,19 @@ +inherit_from: ../.rubocop.yml + +Rails/DynamicFindBy: + Whitelist: + # Capybara finder. + - find_by_id + +Style/FileName: + ExpectMatchingDefinition: true + Exclude: + - "test_helper.rb" + +Style/GlobalVars: + AllowedVariables: + - $api_umbrella_process + - $config + +Style/ClassVars: + Enabled: false diff --git a/test/Gruntfile.js b/test/Gruntfile.js deleted file mode 100644 index 54e82cf5b..000000000 --- a/test/Gruntfile.js +++ /dev/null @@ -1,178 +0,0 @@ -'use strict'; - -module.exports = function(grunt) { - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-mocha-cli'); - - var mochaFiles = [ - 'integration/**/*.js', - 'server/**/*.js', - ]; - if(process.env['MOCHA_FILES']) { - mochaFiles = process.env['MOCHA_FILES'].split(/\s+/); - } - - grunt.initConfig({ - jshint: { - options: { - jshintrc: '.jshintrc' - }, - all: [ - '*.js', - 'factories/**/*.js', - 'integration/**/*.js', - 'server/**/*.js', - 'support/**/*.js', - ], - }, - - mochacli: { - options: { - reporter: 'mocha-multi', - - // Force colors for the output of mutliTest - colors: true, - - // Increase the default timeout from 2 seconds to 4 seconds. We'll - // see if this helps with sporadic issues in the CI environment. - timeout: 4000, - - files: mochaFiles, - }, - multi: { - options: { - reporter: 'mocha-multi', - env: { - multi: 'spec=- xunit=tmp/xunit' + process.env['CIRCLE_NODE_INDEX'] + '.xml', - }, - } - }, - }, - }); - - grunt.registerTask('default', [ - 'test', - ]); - - grunt.registerTask('test', [ - 'jshint', - 'mochacli:multi', - ]); - - // Run the full test suite 20 times. Only print the output when errors are - // encountered. This is to try to make it easier to track down sporadic test - // issues that only happen occasionally. - grunt.registerTask('multiTest', 'Run all the tests multiple times', function() { - var done = this.async(); - - var async = require('async'), - exec = require('child_process').exec, - fs = require('fs'); - - async.timesSeries(20, function(index, next) { - var runNum = index + 1; - process.stdout.write('Run ' + runNum + ' '); - var progress = setInterval(function() { - process.stdout.write('.'); - }, 5000); - - var startTime = process.hrtime(); - var logPath = '/tmp/api-umbrella-multi-test.log'; - exec('./node_modules/.bin/grunt > ' + logPath + ' 2>&1', function(error) { - clearInterval(progress); - - var duration = process.hrtime(startTime); - console.info(' ' + duration[0] + 's'); - - if(error) { - console.info('Run ' + runNum + ' encountered an error: ', error); - console.info(fs.readFileSync(logPath).toString()); - } - - next(error); - }); - }, function(error) { - if(error) { - console.info('Error during multiple runs: ', error); - } - - done(error); - }); - }); - - grunt.registerTask('multiLongConnectionDrops', 'Run all the tests multiple times', function() { - var done = this.async(); - - var async = require('async'), - exec = require('child_process').exec, - fs = require('fs'); - - async.timesSeries(20, function(index, next) { - var runNum = index + 1; - process.stdout.write('Run ' + runNum + ' '); - var progress = setInterval(function() { - process.stdout.write('.'); - }, 5000); - - var startTime = process.hrtime(); - process.env.CONNECTION_DROPS_DURATION = 10 * 60; - var logPath = '/tmp/api-umbrella-multi-long-connection-drops.log'; - exec('./node_modules/.bin/mocha test/integration/dns.js -g "handles ip changes without dropping any connections" > ' + logPath + ' 2>&1', function(error) { - clearInterval(progress); - - var duration = process.hrtime(startTime); - console.info(' ' + duration[0] + 's'); - - if(error) { - console.info('Run ' + runNum + ' encountered an error: ', error); - console.info(fs.readFileSync(logPath).toString()); - } - - next(error); - }); - }, function(error) { - if(error) { - console.info('Error during multiple runs: ', error); - } - - done(error); - }); - }); - - grunt.registerTask('cleanup_logs', 'Re-process any failed or stuck log jobs', function() { - var async = require('async'), - config = require('api-umbrella-config'), - redis = require('redis'); - - var done = this.async(); - var redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host')); - - var queues = ['cv:log_queue:processing', 'cv:log_queue:failed']; - async.eachSeries(queues, function(queue, queueCallback) { - redisClient.zrange(queue, 0, -1, function(error, ids) { - console.info('Re-processing ' + ids.length + ' logs for ' + queue); - - async.eachSeries(ids, function(id, callback) { - redisClient.hgetall('log:' + id, function(error, log) { - if(log) { - console.info('Re-processing ' + id); - var processAt = Date.now(); - redisClient.multi() - .zrem('cv:log_queue:processing', id) - .srem('cv:log_queue:committed', id) - .zadd('log_jobs', processAt, id, callback) - .exec(callback); - } else { - console.info('Cleaning up ' + id); - redisClient.multi() - .zrem('cv:log_queue:processing', id) - .zrem('cv:log_queue:failed', id) - .srem('cv:log_queue:committed', id) - .exec(callback); - } - }); - }, queueCallback); - }); - }, done); - }); -}; diff --git a/test/admin_ui/test_admins.rb b/test/admin_ui/test_admins.rb new file mode 100644 index 000000000..5f8c35ccc --- /dev/null +++ b/test/admin_ui/test_admins.rb @@ -0,0 +1,74 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestAdmins < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_superuser_checkbox_as_superuser_admin + admin_login + visit "/admin/#/admins/new" + + assert_content("Username") + assert_content("Superuser") + end + + def test_superuser_checkbox_as_limited_admin + admin_login(FactoryGirl.create(:limited_admin)) + visit "/admin/#/admins/new" + + assert_content("Username") + refute_content("Superuser") + end + + def test_adds_groups_when_checked + admin_login + + @group1 = FactoryGirl.create(:admin_group) + @group2 = FactoryGirl.create(:admin_group) + @group3 = FactoryGirl.create(:admin_group) + + admin = FactoryGirl.create(:admin) + assert_equal([], admin.group_ids) + + visit "/admin/#/admins/#{admin.id}/edit" + + check @group1.name + check @group3.name + + click_button("Save") + + assert_content("Successfully saved the admin") + + admin = Admin.find(admin.id) + assert_equal([@group1.id, @group3.id].sort, admin.group_ids.sort) + end + + def test_removes_groups_when_checked + admin_login + + @group1 = FactoryGirl.create(:admin_group) + @group2 = FactoryGirl.create(:admin_group) + @group3 = FactoryGirl.create(:admin_group) + + admin = FactoryGirl.create(:admin, :groups => [@group1, @group2]) + assert_equal([@group1.id, @group2.id].sort, admin.group_ids.sort) + + visit "/admin/#/admins/#{admin.id}/edit" + + uncheck @group1.name + uncheck @group2.name + check @group3.name + + click_button("Save") + + assert_content("Successfully saved the admin") + + admin = Admin.find(admin.id) + assert_equal([@group3.id].sort, admin.group_ids.sort) + end +end diff --git a/test/admin_ui/test_api_users.rb b/test/admin_ui/test_api_users.rb new file mode 100644 index 000000000..489a95b94 --- /dev/null +++ b/test/admin_ui/test_api_users.rb @@ -0,0 +1,73 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApiUsers < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_form + admin_login + visit "/admin/#/api_users/new" + + # User Info + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + + # Rate Limiting + select "Custom rate limits", :from => "Rate Limit" + find("button", :text => /Add Rate Limit/).click + within(".custom-rate-limits-table") do + find(".rate-limit-duration-in-units").set("2") + find(".rate-limit-duration-units").select("hours") + find(".rate-limit-limit-by").select("IP Address") + find(".rate-limit-limit").set("1500") + find(".rate-limit-response-headers").click + end + select "Rate limit by IP address", :from => "Limit By" + + # Permissions + fill_in "Roles", :with => "some-user-role" + find(".selectize-dropdown-content div", :text => /Add some-user-role/).click + find("body").native.send_key(:Escape) # Sporadically seems necessary to reset selectize properly for second input. + fill_in "Roles", :with => "some-user-role2" + find(".selectize-dropdown-content div", :text => /Add some-user-role2/).click + fill_in "Restrict Access to IPs", :with => "127.0.0.1\n10.1.1.1/16" + fill_in "Restrict Access to HTTP Referers", :with => "*.example.com/*\n*//example2.com/*" + select "Disabled", :from => "Account Enabled" + + click_button("Save") + assert_content("Successfully saved") + + user = ApiUser.desc(:created_at).first + visit "/admin/#/api_users/#{user.id}/edit" + + # User Info + assert_field("E-mail", :with => "example@example.com") + assert_field("First Name", :with => "John") + assert_field("Last Name", :with => "Doe") + + # Rate Limiting + assert_select("Rate Limit", :selected => "Custom rate limits") + within(".custom-rate-limits-table") do + assert_equal("2", find(".rate-limit-duration-in-units").value) + assert_equal("hours", find(".rate-limit-duration-units").value) + assert_equal("ip", find(".rate-limit-limit-by").value) + assert_equal("1500", find(".rate-limit-limit").value) + assert_equal(true, find(".rate-limit-response-headers").checked?) + end + assert_select("Limit By", :selected => "Rate limit by IP address") + + # Permissions + assert_equal("some-user-role,some-user-role2", find_by_id(find_field("Roles")["data-raw-input-id"], :visible => :all).value) + assert_equal("some-user-role×some-user-role2×", find_by_id(find_field("Roles")["data-selectize-control-id"]).text) + assert_field("Restrict Access to IPs", :with => "127.0.0.1\n10.1.1.1/16") + assert_field("Restrict Access to HTTP Referers", :with => "*.example.com/*\n*//example2.com/*") + assert_select("Account Enabled", :selected => "Disabled") + end +end diff --git a/test/admin_ui/test_api_users_allowed_ips.rb b/test/admin_ui/test_api_users_allowed_ips.rb new file mode 100644 index 000000000..ad0c2d10a --- /dev/null +++ b/test/admin_ui/test_api_users_allowed_ips.rb @@ -0,0 +1,64 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApiUsersAllowedIps < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_empty_input_saves_as_null + admin_login + visit "/admin/#/api_users/new" + + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + click_button("Save") + + assert_content("Successfully saved the user") + user = ApiUser.order_by(:created_at.asc).last + assert_nil(user["settings"]["allowed_ips"]) + end + + def test_multiple_lines_saves_as_array + admin_login + visit "/admin/#/api_users/new" + + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + fill_in "Restrict Access to IPs", :with => "10.0.0.0/8\n\n\n\n127.0.0.1" + click_button("Save") + + assert_content("Successfully saved the user") + user = ApiUser.order_by(:created_at.asc).last + assert_equal(["10.0.0.0/8", "127.0.0.1"], user["settings"]["allowed_ips"]) + end + + def test_displays_existing_array_as_multiple_lines + user = FactoryGirl.create(:api_user, :settings => { :allowed_ips => ["10.0.0.0/24", "10.2.2.2"] }) + admin_login + visit "/admin/#/api_users/#{user.id}/edit" + + assert_equal("10.0.0.0/24\n10.2.2.2", find_field("Restrict Access to IPs").value) + end + + def test_nullifies_existing_array_when_empty_input_saved + user = FactoryGirl.create(:api_user, :settings => { :allowed_ips => ["10.0.0.0/24", "10.2.2.2"] }) + admin_login + visit "/admin/#/api_users/#{user.id}/edit" + + assert_equal("10.0.0.0/24\n10.2.2.2", find_field("Restrict Access to IPs").value) + fill_in "Restrict Access to IPs", :with => "" + click_button("Save") + + assert_content("Successfully saved the user") + user.reload + assert_nil(user["settings"]["allowed_ips"]) + end +end diff --git a/test/admin_ui/test_api_users_allowed_referers.rb b/test/admin_ui/test_api_users_allowed_referers.rb new file mode 100644 index 000000000..3915396a5 --- /dev/null +++ b/test/admin_ui/test_api_users_allowed_referers.rb @@ -0,0 +1,64 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApiUsersAllowedReferers < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_empty_input_saves_as_null + admin_login + visit "/admin/#/api_users/new" + + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + click_button("Save") + + assert_content("Successfully saved the user") + user = ApiUser.order_by(:created_at.asc).last + assert_nil(user["settings"]["allowed_referers"]) + end + + def test_multiple_lines_saves_as_array + admin_login + visit "/admin/#/api_users/new" + + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + fill_in "Restrict Access to HTTP Referers", :with => "*.example.com/*\n\n\n\nhttp://google.com/*" + click_button("Save") + + assert_content("Successfully saved the user") + user = ApiUser.order_by(:created_at.asc).last + assert_equal(["*.example.com/*", "http://google.com/*"], user["settings"]["allowed_referers"]) + end + + def test_displays_existing_array_as_multiple_lines + user = FactoryGirl.create(:api_user, :settings => { :allowed_referers => ["*.example.com/*", "http://google.com/*"] }) + admin_login + visit "/admin/#/api_users/#{user.id}/edit" + + assert_equal("*.example.com/*\nhttp://google.com/*", find_field("Restrict Access to HTTP Referers").value) + end + + def test_nullifies_existing_array_when_empty_input_saved + user = FactoryGirl.create(:api_user, :settings => { :allowed_referers => ["*.example.com/*", "http://google.com/*"] }) + admin_login + visit "/admin/#/api_users/#{user.id}/edit" + + assert_equal("*.example.com/*\nhttp://google.com/*", find_field("Restrict Access to HTTP Referers").value) + fill_in "Restrict Access to HTTP Referers", :with => "" + click_button("Save") + + assert_content("Successfully saved the user") + user.reload + assert_nil(user["settings"]["allowed_referers"]) + end +end diff --git a/test/admin_ui/test_api_users_api_key_visibility.rb b/test/admin_ui/test_api_users_api_key_visibility.rb new file mode 100644 index 000000000..0400d15bc --- /dev/null +++ b/test/admin_ui/test_api_users_api_key_visibility.rb @@ -0,0 +1,59 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApiUsersApiKeyVisibility < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_api_key_in_create_notification + @user = FactoryGirl.create(:xss_api_user) + admin_login + visit "/admin/#/api_users/new" + + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + click_button("Save") + + assert_content("Successfully saved the user") + user = ApiUser.order_by(:created_at.asc).last + assert_equal("Doe", user.last_name) + assert_content(user.api_key) + end + + def test_api_key_can_be_revealed_when_admin_has_permissions + admin = FactoryGirl.create(:admin) + user = FactoryGirl.create(:api_user, :created_by => admin.id, :created_at => Time.now.utc - 2.weeks + 5.minutes) + admin_login(admin) + visit "/admin/#/api_users/#{user.id}/edit" + + assert_content(user.api_key_preview) + refute_content(user.api_key) + assert_link("(reveal)") + click_link("(reveal)") + assert_content(user.api_key) + refute_content(user.api_key_preview) + refute_link("(reveal)") + assert_link("(hide)") + click_link("(hide)") + assert_content(user.api_key_preview) + refute_content(user.api_key) + assert_link("(reveal)") + end + + def test_api_key_is_hidden_when_admin_lacks_permissions + admin = FactoryGirl.create(:limited_admin) + user = FactoryGirl.create(:api_user, :created_by => admin.id, :created_at => Time.now.utc - 2.weeks - 5.minutes) + admin_login(admin) + visit "/admin/#/api_users/#{user.id}/edit" + + assert_content(user.api_key_preview) + refute_content(user.api_key) + refute_link("(reveal)") + end +end diff --git a/test/admin_ui/test_api_users_rate_limits.rb b/test/admin_ui/test_api_users_rate_limits.rb new file mode 100644 index 000000000..aa107b03c --- /dev/null +++ b/test/admin_ui/test_api_users_rate_limits.rb @@ -0,0 +1,66 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApiUsersRateLimits < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_edit_custom_rate_limits + user = FactoryGirl.create(:custom_rate_limit_api_user) + admin_login + visit "/admin/#/api_users/#{user.id}/edit" + + within(".custom-rate-limits-table") do + assert_equal("1", find(".rate-limit-duration-in-units").value) + assert_equal("minutes", find(".rate-limit-duration-units").value) + assert_equal("ip", find(".rate-limit-limit-by").value) + assert_equal("500", find(".rate-limit-limit").value) + assert_equal(true, find(".rate-limit-response-headers").checked?) + + find(".rate-limit-limit").set("200") + end + + click_button("Save") + assert_content("Successfully saved") + + user.reload + + assert_equal(1, user.settings.rate_limits.length) + rate_limit = user.settings.rate_limits.first + assert_equal(200, rate_limit.limit) + end + + def test_remove_custom_rate_limits + user = FactoryGirl.create(:api_user, { + :settings => FactoryGirl.build(:custom_rate_limit_api_setting, { + :rate_limits => [ + FactoryGirl.attributes_for(:api_rate_limit, :duration => 5000, :limit => 10), + FactoryGirl.attributes_for(:api_rate_limit, :duration => 10000, :limit => 20), + ], + }), + }) + + assert_equal(2, user.settings.rate_limits.length) + + admin_login + visit "/admin/#/api_users/#{user.id}/edit" + + within(".custom-rate-limits-table") do + click_link("Remove", :match => :first) + end + click_button("OK") + + click_button("Save") + assert_content("Successfully saved") + + user.reload + + assert_equal(1, user.settings.rate_limits.length) + rate_limit = user.settings.rate_limits.first + assert_equal(20, rate_limit.limit) + end +end diff --git a/test/admin_ui/test_api_users_welcome_email.rb b/test/admin_ui/test_api_users_welcome_email.rb new file mode 100644 index 000000000..3b1c13cd9 --- /dev/null +++ b/test/admin_ui/test_api_users_welcome_email.rb @@ -0,0 +1,44 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApiUsersWelcomeEmail < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + include ApiUmbrellaTestHelpers::DelayedJob + + def setup + setup_server + + response = Typhoeus.delete("http://127.0.0.1:13103/api/v1/messages") + assert_response_code(200, response) + end + + def test_no_email_by_default + admin_login + visit "/admin/#/api_users/new" + + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + click_button("Save") + assert_content("Successfully saved the user") + + assert_equal(0, delayed_job_sent_messages.length) + end + + def test_email_when_explicitly_requested + admin_login + visit "/admin/#/api_users/new" + + fill_in "E-mail", :with => "example@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + check "User agrees to the terms and conditions" + check "Send user welcome e-mail with API key information" + click_button("Save") + assert_content("Successfully saved the user") + + assert_equal(1, delayed_job_sent_messages.length) + end +end diff --git a/test/admin_ui/test_api_users_xss.rb b/test/admin_ui/test_api_users_xss.rb new file mode 100644 index 000000000..d81554f43 --- /dev/null +++ b/test/admin_ui/test_api_users_xss.rb @@ -0,0 +1,49 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApiUsersXss < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_xss_escaping_in_table + @user = FactoryGirl.create(:xss_api_user) + admin_login + visit "/admin/#/api_users" + + assert_content(@user.email) + assert_content(@user.first_name) + assert_content(@user.last_name) + assert_content(@user.use_description) + assert_content(@user.registration_source) + refute_selector(".xss-test", :visible => :all) + end + + def test_xss_escaping_in_form + @user = FactoryGirl.create(:xss_api_user) + admin_login + visit "/admin/#/api_users/#{@user.id}/edit" + + assert_equal(@user.email, find_field("E-mail").value) + assert_equal(@user.first_name, find_field("First Name").value) + assert_equal(@user.last_name, find_field("Last Name").value) + assert_equal(@user.use_description, find_field("Purpose").value) + assert_content(@user.registration_source) + refute_selector(".xss-test", :visible => :all) + end + + def test_xss_escaping_in_flash_confirmation_message + @user = FactoryGirl.create(:xss_api_user) + admin_login + visit "/admin/#/api_users/#{@user.id}/edit" + + fill_in "Last Name", :with => "Doe" + click_button("Save") + + assert_content("Successfully saved the user \"#{@user.email}\"") + refute_selector(".xss-test", :visible => :all) + end +end diff --git a/test/admin_ui/test_apis.rb b/test/admin_ui/test_apis.rb new file mode 100644 index 000000000..4128e259a --- /dev/null +++ b/test/admin_ui/test_apis.rb @@ -0,0 +1,380 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApis < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + + Api.delete_all + end + + def test_saves_when_only_nested_fields_change + api = FactoryGirl.create(:api_with_settings, :name => "Save Test API") + assert_nil(api.settings.error_data) + + admin_login + visit "/admin/#/apis" + click_link "Save Test API" + + assert_equal("Save Test API", find_field("Name").value) + + find("legend a", :text => /Advanced Settings/).click + fill_in "API Key Missing", :with => "hello1: foo\nhello2: bar", :visible => :all + + click_button("Save") + assert_content("Successfully saved") + + api = Api.find(api.id) + assert_equal({ + "api_key_missing" => { + "hello1" => "foo", + "hello2" => "bar", + }, + }, api.settings.error_data) + end + + def test_loads_from_server_on_each_load + api = FactoryGirl.create(:api_with_settings, :name => "Test Load API", :frontend_host => "example1.com") + admin_login + visit "/admin/#/apis" + assert_content("Add API Backend") + + click_link "Test Load API" + assert_equal("example1.com", find_field("Frontend Host").value) + + find("nav a", :text => /Configuration/).click + find("nav a", :text => /API Backends/).click + assert_content("Add API Backend") + + api.frontend_host = "example2.com" + api.save! + + click_link "Test Load API" + assert_equal("example2.com", find_field("Frontend Host").value) + end + + def test_validation_error_when_all_servers_removed_from_existing_api + api = FactoryGirl.create(:api) + admin_login + visit "/admin/#/apis/#{api.id}/edit" + find("#servers_table a", :text => /Remove/).click + click_button("OK") + click_button("Save") + assert_content("Must have at least one servers") + + api = Api.find(api.id) + assert_equal(1, api.servers.length) + end + + def test_validation_error_when_all_url_prefixes_removed_from_existing_api + api = FactoryGirl.create(:api) + admin_login + visit "/admin/#/apis/#{api.id}/edit" + find("#url_matches_table a", :text => /Remove/).click + click_button("OK") + click_button("Save") + assert_content("Must have at least one url_matches") + + api = Api.find(api.id) + assert_equal(1, api.url_matches.length) + end + + def test_roles_override_checkbox_only_in_sub_settings + admin_login + visit "/admin/#/apis/new" + + find("legend a", :text => /Global Request Settings/).click + refute_field('Override required roles from "Global Request Settings"') + + find("legend a", :text => /Sub-URL Request Settings/).click + find("button", :text => /Add URL Settings/).click + assert_selector(".modal") + within(".modal") do + assert_field('Override required roles from "Global Request Settings"') + end + end + + def test_defaults_frontend_host_to_current_url_hostname + admin_login + visit "/admin/#/apis/new" + assert_field("Frontend Host", :with => "127.0.0.1") + end + + def test_form + admin_login + visit "/admin/#/apis/new" + + fill_in "Name", :with => "Testing API Backend" + + # Backend + select "https", :from => "Backend Protocol" + find("button", :text => /Add Server/).click + assert_selector(".modal") + within(".modal") do + fill_in "Host", :with => "google.com" + fill_in "Port", :with => "443" + click_button("OK") + end + + # Host + fill_in "Frontend Host", :with => "api.foo.com" + fill_in "Backend Host", :with => "api.bar.com" + + # Matching URL Prefixes + find("button", :text => /Add URL Prefix/).click + assert_selector(".modal") + within(".modal") do + fill_in "Frontend Prefix", :with => "/foo" + fill_in "Backend Prefix", :with => "/bar" + click_button("OK") + end + + # Global Request Settings + find("legend a", :text => /Global Request Settings/).click + fill_in "Append Query String Parameters", :with => "foo=bar" + fill_in "Set Request Headers", :with => "X-Foo1: Bar\nX-Bar2: Foo" + fill_in "HTTP Basic Authentication", :with => "foo:bar" + select "Optional - HTTPS is optional", :from => "HTTPS Requirements" + select "Disabled - API keys are optional", :from => "API Key Checks" + select "None - API keys can be used without any verification", :from => "API Key Verification Requirements" + fill_in "Required Roles", :with => "some-role" + find(".selectize-dropdown-content div.create", :text => /Add some-role/).click + find("body").send_keys(:escape) + fill_in "Required Roles", :with => "some-role2" + find(".selectize-dropdown-content div.create", :text => /Add some-role2/).click + find("body").send_keys(:escape) + check "Via HTTP header" + check "Via GET query parameter" + select "Custom rate limits", :from => "Rate Limit" + find("button", :text => /Add Rate Limit/).click + within(".custom-rate-limits-table") do + find(".rate-limit-duration-in-units").set("2") + find(".rate-limit-duration-units").select("hours") + find(".rate-limit-limit-by").select("IP Address") + find(".rate-limit-limit").set("1500") + find(".rate-limit-response-headers").click + end + select "IP Only - API key rate limits are ignored (only IP based limits are applied)", :from => "Anonymous Rate Limit Behavior" + select "API Key Only - IP based rate limits are ignored (only API key limits are applied)", :from => "Authenticated Rate Limit Behavior" + fill_in "Default Response Headers", :with => "X-Foo2: Bar\nX-Bar2: Foo" + fill_in "Override Response Headers", :with => "X-Foo3: Bar\nX-Bar3: Foo" + + # Sub-URL Request Settings + find("legend a", :text => /Sub-URL Request Settings/).click + find("button", :text => /Add URL Settings/).click + assert_selector(".modal") + within(".modal") do + select "OPTIONS", :from => "HTTP Method" + fill_in "Regex", :with => "^/foo.*" + select "Required & return message - HTTP requests will receive a message to use HTTPS", :from => "HTTPS Requirements" + select "Disabled - API keys are optional", :from => "API Key Checks" + select "E-mail verification required - Existing API keys will break, only new API keys will work if verified", :from => "API Key Verification Requirements" + sleep 1 + fill_in "Required Roles", :with => "sub-role" + end + find(".selectize-dropdown-content div.create", :text => /Add sub-role/).click + find("body").send_keys(:escape) + within(".modal") do + check 'Override required roles from "Global Request Settings"' + check "Via HTTP header" + check "Via GET query parameter" + select "Custom rate limits", :from => "Rate Limit" + find("button", :text => /Add Rate Limit/).click + within(".custom-rate-limits-table") do + find(".rate-limit-duration-in-units").set("3") + find(".rate-limit-duration-units").select("minutes") + find(".rate-limit-limit-by").select("IP Address") + find(".rate-limit-limit").set("100") + find(".rate-limit-response-headers").click + end + select "IP Only - API key rate limits are ignored (only IP based limits are applied)", :from => "Anonymous Rate Limit Behavior" + select "API Key Only - IP based rate limits are ignored (only API key limits are applied)", :from => "Authenticated Rate Limit Behavior" + fill_in "Default Response Headers", :with => "X-Sub-Foo2: Bar\nX-Sub-Bar2: Foo" + fill_in "Override Response Headers", :with => "X-Sub-Foo3: Bar\nX-Sub-Bar3: Foo" + click_button("OK") + end + + # Advanced Requests Rewriting + find("legend a", :text => /Advanced Requests Rewriting/).click + find("button", :text => /Add Rewrite/).click + assert_selector(".modal") + within(".modal") do + select "Regular Expression", :from => "Matcher Type" + select "PUT", :from => "HTTP Method" + fill_in "Frontend Matcher", :with => "[0-9]+" + fill_in "Backend Replacement", :with => "number" + click_button("OK") + end + + # Advanced Settings + find("legend a", :text => /Advanced Settings/).click + fill_in "JSON Template", :with => '{"foo":"bar"}', :visible => :all + fill_in "XML Template", :with => "bar", :visible => :all + fill_in "CSV Template", :with => "foo,bar\nbar,foo", :visible => :all + fill_in "Common (All Errors)", :with => "foo0: bar0\nbar0: foo0", :visible => :all + fill_in "API Key Missing", :with => "foo1: bar1\nbar1: foo1", :visible => :all + fill_in "API Key Invalid", :with => "foo2: bar2\nbar2: foo2", :visible => :all + fill_in "API Key Disabled", :with => "foo3: bar3\nbar3: foo3", :visible => :all + fill_in "API Key Unauthorized", :with => "foo4: bar4\nbar4: foo4", :visible => :all + fill_in "Over Rate Limit", :with => "foo5: bar5\nbar5: foo5", :visible => :all + fill_in "HTTPS Required", :with => "foo6: bar6\nbar6: foo6", :visible => :all + + click_button("Save") + assert_content("Successfully saved") + + api = Api.desc(:created_at).first + visit "/admin/#/apis/#{api.id}/edit" + + assert_field("Name", :with => "Testing API Backend") + + # Backend + assert_select("Backend Protocol", :selected => "https") + find("#servers_table a", :text => /Edit/).click + assert_selector(".modal") + within(".modal") do + assert_field("Host", :with => "google.com") + assert_field("Port", :with => "443") + click_button("OK") + end + + # Host + assert_field("Frontend Host", :with => "api.foo.com") + assert_field("Backend Host", :with => "api.bar.com") + + # Matching URL Prefixes + find("#url_matches_table a", :text => /Edit/).click + assert_selector(".modal") + within(".modal") do + assert_field("Frontend Prefix", :with => "/foo") + assert_field("Backend Prefix", :with => "/bar") + click_button("OK") + end + + # Global Request Settings + find("legend a", :text => /Global Request Settings/).click + assert_field("Append Query String Parameters", :with => "foo=bar") + assert_field("Set Request Headers", :with => "X-Foo1: Bar\nX-Bar2: Foo") + assert_field("HTTP Basic Authentication", :with => "foo:bar") + assert_select("HTTPS Requirements", :selected => "Optional - HTTPS is optional") + assert_select("API Key Checks", :selected => "Disabled - API keys are optional") + assert_select("API Key Verification Requirements", :selected => "None - API keys can be used without any verification") + field = find_field("Required Roles") + assert_selector("#" + field["data-selectize-control-id"], :text => "some-role×some-role2×") + assert_equal("some-role,some-role2", find_by_id(field["data-raw-input-id"], :visible => :all).value) + assert_checked_field("Via HTTP header") + assert_checked_field("Via GET query parameter") + assert_select("Rate Limit", :selected => "Custom rate limits") + within(".custom-rate-limits-table") do + assert_equal("2", find(".rate-limit-duration-in-units").value) + assert_equal("hours", find(".rate-limit-duration-units").value) + assert_equal("ip", find(".rate-limit-limit-by").value) + assert_equal("1500", find(".rate-limit-limit").value) + assert_equal(true, find(".rate-limit-response-headers").checked?) + end + assert_select("Anonymous Rate Limit Behavior", :selected => "IP Only - API key rate limits are ignored (only IP based limits are applied)") + assert_select("Authenticated Rate Limit Behavior", :selected => "API Key Only - IP based rate limits are ignored (only API key limits are applied)") + assert_field("Default Response Headers", :with => "X-Foo2: Bar\nX-Bar2: Foo") + assert_field("Override Response Headers", :with => "X-Foo3: Bar\nX-Bar3: Foo") + + # Sub-URL Request Settings + find("legend a", :text => /Sub-URL Request Settings/).click + find("#sub_settings_table a", :text => /Edit/).click + assert_selector(".modal") + within(".modal") do + assert_select("HTTP Method", :selected => "OPTIONS") + assert_field("Regex", :with => "^/foo.*") + assert_select("HTTPS Requirements", :selected => "Required & return message - HTTP requests will receive a message to use HTTPS") + assert_select("API Key Checks", :selected => "Disabled - API keys are optional") + assert_select("API Key Verification Requirements", :selected => "E-mail verification required - Existing API keys will break, only new API keys will work if verified") + field = find_field("Required Roles") + assert_selector("#" + field["data-selectize-control-id"], :text => "sub-role×") + assert_equal("sub-role", find_by_id(field["data-raw-input-id"], :visible => :all).value) + assert_checked_field('Override required roles from "Global Request Settings"') + assert_checked_field("Via HTTP header") + assert_checked_field("Via GET query parameter") + assert_select("Rate Limit", :selected => "Custom rate limits") + within(".custom-rate-limits-table") do + assert_equal("3", find(".rate-limit-duration-in-units").value) + assert_equal("minutes", find(".rate-limit-duration-units").value) + assert_equal("ip", find(".rate-limit-limit-by").value) + assert_equal("100", find(".rate-limit-limit").value) + assert_equal(true, find(".rate-limit-response-headers").checked?) + end + assert_select("Anonymous Rate Limit Behavior", :selected => "IP Only - API key rate limits are ignored (only IP based limits are applied)") + assert_select("Authenticated Rate Limit Behavior", :selected => "API Key Only - IP based rate limits are ignored (only API key limits are applied)") + assert_field("Default Response Headers", :with => "X-Sub-Foo2: Bar\nX-Sub-Bar2: Foo") + assert_field("Override Response Headers", :with => "X-Sub-Foo3: Bar\nX-Sub-Bar3: Foo") + click_button("OK") + end + + # Advanced Requests Rewriting + find("legend a", :text => /Advanced Requests Rewriting/).click + find("#rewrites a", :text => /Edit/).click + assert_selector(".modal") + within(".modal") do + assert_select("Matcher Type", :selected => "Regular Expression") + assert_select("HTTP Method", :selected => "PUT") + assert_field("Frontend Matcher", :with => "[0-9]+") + assert_field("Backend Replacement", :with => "number") + click_button("OK") + end + + # Advanced Settings + find("legend a", :text => /Advanced Settings/).click + field = find_field("JSON Template", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => '{"foo":"bar"}') + assert_equal('{"foo":"bar"}', find_by_id(field["data-raw-input-id"], :visible => :all).value) + field = find_field("XML Template", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => "bar") + assert_equal("bar", find_by_id(field["data-raw-input-id"], :visible => :all).value) + field = find_field("CSV Template", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => "foo,bar bar,foo") + assert_equal("foo,bar\nbar,foo", find_by_id(field["data-raw-input-id"], :visible => :all).value) + field = find_field("API Key Missing", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => "foo1: bar1 bar1: foo1") + assert_equal("foo1: bar1\nbar1: foo1", find_by_id(field["data-raw-input-id"], :visible => :all).value) + field = find_field("API Key Invalid", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => "foo2: bar2 bar2: foo2") + assert_equal("foo2: bar2\nbar2: foo2", find_by_id(field["data-raw-input-id"], :visible => :all).value) + field = find_field("API Key Disabled", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => "foo3: bar3 bar3: foo3") + assert_equal("foo3: bar3\nbar3: foo3", find_by_id(field["data-raw-input-id"], :visible => :all).value) + field = find_field("API Key Unauthorized", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => "foo4: bar4 bar4: foo4") + assert_equal("foo4: bar4\nbar4: foo4", find_by_id(field["data-raw-input-id"], :visible => :all).value) + field = find_field("Over Rate Limit", :visible => :all) + assert_selector("#" + field["data-ace-content-id"], :text => "foo5: bar5 bar5: foo5") + assert_equal("foo5: bar5\nbar5: foo5", find_by_id(field["data-raw-input-id"], :visible => :all).value) + end + + def test_edit_custom_rate_limits + api = FactoryGirl.create(:api, { + :settings => FactoryGirl.build(:custom_rate_limit_api_setting), + }) + admin_login + visit "/admin/#/apis/#{api.id}/edit" + + find("legend a", :text => /Global Request Settings/).click + within(".custom-rate-limits-table") do + assert_equal("1", find(".rate-limit-duration-in-units").value) + assert_equal("minutes", find(".rate-limit-duration-units").value) + assert_equal("ip", find(".rate-limit-limit-by").value) + assert_equal("500", find(".rate-limit-limit").value) + assert_equal(true, find(".rate-limit-response-headers").checked?) + + find(".rate-limit-limit").set("200") + end + + click_button("Save") + assert_content("Successfully saved") + + api.reload + + assert_equal(1, api.settings.rate_limits.length) + rate_limit = api.settings.rate_limits.first + assert_equal(200, rate_limit.limit) + end +end diff --git a/test/admin_ui/test_apis_reordering.rb b/test/admin_ui/test_apis_reordering.rb new file mode 100644 index 000000000..9f467c5ca --- /dev/null +++ b/test/admin_ui/test_apis_reordering.rb @@ -0,0 +1,88 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestApisReordering < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + + Api.delete_all + FactoryGirl.create(:api, :name => "API A", :sort_order => 3) + FactoryGirl.create(:api, :name => "API B", :sort_order => 1) + FactoryGirl.create(:api, :name => "API C", :sort_order => 2) + FactoryGirl.create(:api, :name => "API testing-filter", :sort_order => 4) + end + + def test_toggle_drag_handles + admin_login + visit "/admin/#/apis" + + refute_selector("tbody td.reorder-handle") + click_button "Reorder" + assert_selector("tbody td.reorder-handle", :count => 4) + click_button "Done" + refute_selector("tbody td.reorder-handle") + end + + def test_remove_filters_while_reordering + admin_login + visit "/admin/#/apis" + + assert_selector("tbody tr", :count => 4) + find(".dataTables_filter input").set("testing-fi") + assert_selector("tbody tr", :count => 1) + click_button "Reorder" + assert_selector("tbody tr", :count => 4) + end + + def test_forces_sorting_while_reordering + admin_login + visit "/admin/#/apis" + + assert_selector("tbody tr:first-child td:first-child", :text => "API A") + click_button "Reorder" + assert_selector("tbody tr:first-child td:first-child", :text => "API B") + end + + def test_exits_reorder_mode_on_filter + admin_login + visit "/admin/#/apis" + + click_button "Reorder" + assert_selector("tbody td.reorder-handle", :count => 4) + find(".dataTables_filter input").set("testing-fi") + refute_selector("tbody td.reorder-handle") + end + + def test_exit_reorder_mode_on_sort + admin_login + visit "/admin/#/apis" + + click_button "Reorder" + assert_selector("tbody td.reorder-handle", :count => 4) + find("thead tr:first-child").click + refute_selector("tbody td.reorder-handle") + end + + def test_reordering_on_drag + admin_login + visit "/admin/#/apis" + + names = Api.order_by(:sort_order.asc).all.map { |api| api.name } + assert_equal(["API B", "API C", "API A", "API testing-filter"], names) + + click_button "Reorder" + + refute_selector(".busy-blocker") + assert_selector("tbody tr:first-child td:first-child", :text => "API B") + handle = find("tbody td:first-child", :text => "API A").find(:xpath, "..").find("td.reorder-handle") + handle.native.drag_by(0, -70) + assert_selector("tbody tr:first-child td:first-child", :text => "API A") + refute_selector(".busy-blocker") + + names = Api.order_by(:sort_order.asc).all.map { |api| api.name } + assert_equal(["API A", "API B", "API C", "API testing-filter"], names) + end +end diff --git a/test/admin_ui/test_config_publish_pending.rb b/test/admin_ui/test_config_publish_pending.rb new file mode 100644 index 000000000..d65aebe49 --- /dev/null +++ b/test/admin_ui/test_config_publish_pending.rb @@ -0,0 +1,181 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestConfigPublishPending < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + include Minitest::Hooks + + def setup + setup_server + Api.delete_all + WebsiteBackend.delete_all + ConfigVersion.delete_all + end + + def after_all + super + default_config_version_needed + end + + def test_pending_changes_grouped_into_categories + FactoryGirl.create(:api) + deleted_api = FactoryGirl.create(:api) + modified_api = FactoryGirl.create(:api, :name => "Before") + ConfigVersion.publish!(ConfigVersion.pending_config) + deleted_api.update_attribute(:deleted_at, Time.now.utc) + modified_api.update_attribute(:name, "After") + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + assert_text("1 Deleted API Backends") + assert_text("1 Modified API Backends") + assert_text("1 New API Backends") + end + + def test_hides_categories_without_changes + ConfigVersion.publish!(ConfigVersion.pending_config) + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + refute_text("Deleted API Backends") + refute_text("Modified API Backends") + assert_text("New API Backends") + end + + def test_message_when_no_changes_to_publish + FactoryGirl.create(:api) + ConfigVersion.publish!(ConfigVersion.pending_config) + + admin_login + visit "/admin/#/config/publish" + assert_text("Published configuration is up to date") + end + + def test_diff_of_config_changes + api = FactoryGirl.create(:api, :name => "Before") + ConfigVersion.publish!(ConfigVersion.pending_config) + api.update_attribute(:name, "After") + + admin_login + visit "/admin/#/config/publish" + assert_selector(".config-diff", :visible => :hidden) + click_link("View Config Differences") + assert_selector(".config-diff", :visible => :visible) + assert_selector(".config-diff del", :text => "Before") + assert_selector(".config-diff ins", :text => "After") + end + + def test_auto_selection_for_single_change + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + assert_selector("input[type=checkbox][name*=publish]", :count => 1) + assert_selector("input[type=checkbox][name*=publish]:checked", :count => 1) + end + + def test_no_auto_selection_for_multiple_changes + FactoryGirl.create(:api) + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + assert_selector("input[type=checkbox][name*=publish]", :count => 2) + refute_selector("input[type=checkbox][name*=publish]:checked") + end + + def test_refreshes_changes_on_load + FactoryGirl.create(:api) + ConfigVersion.publish!(ConfigVersion.pending_config) + + admin_login + visit "/admin/#/config/publish" + refute_text("New API Backends") + + find("nav a", :text => /Configuration/).click + find("nav a", :text => /API Backends/).click + assert_content("Add API Backend") + + FactoryGirl.create(:api) + find("nav a", :text => /Configuration/).click + find("nav a", :text => /Publish Changes/).click + assert_text("1 New API Backends") + end + + def test_check_or_uncheck_all_link + FactoryGirl.create(:api) + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + + assert_selector("input[type=checkbox][name*=publish]", :count => 2) + refute_text("Uncheck all") + assert_text("Check all") + refute_selector("input[type=checkbox][name*=publish]:checked") + + click_link("Check all") + assert_selector("input[type=checkbox][name*=publish]:checked") + refute_text("Check all") + assert_text("Uncheck all") + + click_link("Uncheck all") + refute_selector("input[type=checkbox][name*=publish]:checked") + refute_text("Uncheck all") + assert_text("Check all") + + checkboxes = all("input[type=checkbox][name*=publish]") + checkboxes[0].click + assert_text("Check all") + checkboxes[1].click + assert_text("Uncheck all") + checkboxes[1].click + assert_text("Check all") + end + + def test_disables_publish_button_when_no_changes_checked + FactoryGirl.create(:api) + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + + assert_selector("input[type=checkbox][name*=publish]", :count => 2) + refute_selector("input[type=checkbox][name*=publish]:checked") + publish_button = find("#publish_button") + checkbox = all("input[type=checkbox][name*=publish]")[0] + + assert_equal(false, checkbox[:checked]) + assert_equal(true, publish_button.disabled?) + + checkbox.click + assert_equal(true, checkbox[:checked]) + assert_equal(false, publish_button.disabled?) + + checkbox.click + assert_equal(false, checkbox[:checked]) + assert_equal(true, publish_button.disabled?) + end + + def test_enables_publish_button_on_load_if + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + + assert_selector("input[type=checkbox][name*=publish]", :count => 1) + assert_selector("input[type=checkbox][name*=publish]:checked", :count => 1) + publish_button = find("#publish_button") + checkbox = all("input[type=checkbox][name*=publish]")[0] + + assert_equal(true, checkbox[:checked]) + assert_equal(false, publish_button.disabled?) + + checkbox.click + assert_equal(false, checkbox[:checked]) + assert_equal(true, publish_button.disabled?) + end +end diff --git a/test/admin_ui/test_config_publish_submit.rb b/test/admin_ui/test_config_publish_submit.rb new file mode 100644 index 000000000..3c313c68f --- /dev/null +++ b/test/admin_ui/test_config_publish_submit.rb @@ -0,0 +1,50 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestConfigPublishSubmit < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + include Minitest::Hooks + + def setup + setup_server + Api.delete_all + WebsiteBackend.delete_all + ConfigVersion.delete_all + end + + def after_all + super + default_config_version_needed + end + + def test_publishing_changes + api = FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + click_button("Publish") + assert_text("Successfully published the configuration") + + assert_text("Published configuration is up to date") + active_config = ConfigVersion.active_config + assert_equal(1, active_config["apis"].length) + assert_equal(api.id, active_config["apis"].first["_id"]) + end + + def test_publishing_only_selected_changes + api1 = FactoryGirl.create(:api) + FactoryGirl.create(:api) + + admin_login + visit "/admin/#/config/publish" + check("config[apis][#{api1.id}][publish]") + click_button("Publish") + + refute_text("Published configuration is up to date") + assert_text("1 New API Backends") + active_config = ConfigVersion.active_config + assert_equal(1, active_config["apis"].length) + assert_equal(api1.id, active_config["apis"].first["_id"]) + end +end diff --git a/test/admin_ui/test_datatables.rb b/test/admin_ui/test_datatables.rb new file mode 100644 index 000000000..0179db8fb --- /dev/null +++ b/test/admin_ui/test_datatables.rb @@ -0,0 +1,49 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestDatatables < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_uses_placeholder_not_label_for_search_field + admin_login + visit "/admin/#/api_users" + assert_equal("", find(".dataTables_filter").text) + assert_equal("Search...", find(".dataTables_filter input")[:placeholder]) + end + + def test_spinner_on_server_side_loads + admin_login + visit "/admin/" + delay_all_ajax_calls + + find("nav a", :text => /Users/).click + find("nav a", :text => /API Users/).click + + assert_selector(".busy-blocker") + refute_selector(".busy-blocker") + + find("thead tr:first-child").click + assert_selector(".busy-blocker") + refute_selector(".busy-blocker") + end + + private + + def delay_all_ajax_calls(delay = 1000) + page.execute_script <<-eos + $.ajaxOrig = $.ajax; + $.ajax = function() { + var args = arguments; + var self = this; + setTimeout(function() { + $.ajaxOrig.apply(self, args); + }, #{delay}); + }; + eos + end +end diff --git a/test/admin_ui/test_elasticsearch_proxy.rb b/test/admin_ui/test_elasticsearch_proxy.rb new file mode 100644 index 000000000..1768fb55e --- /dev/null +++ b/test/admin_ui/test_elasticsearch_proxy.rb @@ -0,0 +1,60 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestElasticsearchProxy < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_redirect_to_login_for_unauthenticated_requests + visit "/admin/elasticsearch" + assert_content("You need to sign in") + refute_content('"lucene_version"') + assert_match(%r{/admin/login\z}, page.current_url) + + visit "/admin/elasticsearch/_search" + assert_content("You need to sign in") + refute_content('"hits"') + assert_match(%r{/admin/login\z}, page.current_url) + end + + def test_forbidden_for_unauthorized_admins + admin_login(FactoryGirl.create(:limited_admin)) + + visit "/admin/elasticsearch" + assert_equal(403, page.status_code) + assert_content("Forbidden") + refute_content('"lucene_version"') + + visit "/admin/elasticsearch/_search" + assert_equal(403, page.status_code) + assert_content("Forbidden") + refute_content('"hits"') + end + + def test_allowed_for_superuser_admins + admin_login + + visit "/admin/elasticsearch" + assert_equal(200, page.status_code) + assert_content('"lucene_version"') + + visit "/admin/elasticsearch/_search" + assert_equal(200, page.status_code) + assert_content('"hits"') + + # Redirect rewriting + response = Typhoeus.get("https://127.0.0.1:9081/admin/elasticsearch/_plugin/foobar", keyless_http_options.deep_merge({ + :headers => { + "Cookie" => "_api_umbrella_session=#{page.driver.cookies["_api_umbrella_session"].value}", + }, + })) + assert_response_code(301, response) + assert_equal("/admin/elasticsearch/_plugin/foobar/", response.headers["Location"]) + assert_match(%r{URL=/admin/elasticsearch/_plugin/foobar/>}, response.body) + assert_equal(response.body.bytesize, response.headers["Content-Length"].to_i) + end +end diff --git a/test/admin_ui/test_legacy_redirects.rb b/test/admin_ui/test_legacy_redirects.rb new file mode 100644 index 000000000..ef2e86e25 --- /dev/null +++ b/test/admin_ui/test_legacy_redirects.rb @@ -0,0 +1,105 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestLegacyRedirects < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + + ElasticsearchHelper.clean_es_indices(["2014-11", "2015-01", "2015-03"]) + FactoryGirl.create(:log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z").utc) + LogItem.gateway.refresh_index! + end + + def test_drilldown + admin_login + visit "/admin/#/stats/drilldown/tz=America%2FDenver&search=&start_at=2015-01-15&end_at=2015-01-18&query=%7B%22condition%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22id%22%3A%22gatekeeper_denied_code%22%2C%22field%22%3A%22gatekeeper_denied_code%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22select%22%2C%22operator%22%3A%22is_null%22%2C%22value%22%3Anull%7D%2C%7B%22id%22%3A%22request_host%22%2C%22field%22%3A%22request_host%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22text%22%2C%22operator%22%3A%22begins_with%22%2C%22value%22%3A%22example.com%22%7D%5D%7D&interval=hour&beta_analytics=false®ion=US" + assert_link("Download CSV", :href => /start_at=2015-01-15/) + + uri = Addressable::URI.parse(page.current_url) + assert_equal("/admin/", uri.path) + assert(uri.fragment) + + fragment_uri = Addressable::URI.parse(uri.fragment) + assert_equal("/stats/drilldown", fragment_uri.path) + assert_equal({ + "start_at" => "2015-01-15", + "end_at" => "2015-01-18", + "interval" => "hour", + "query" => "{\"condition\":\"AND\",\"rules\":[{\"id\":\"gatekeeper_denied_code\",\"field\":\"gatekeeper_denied_code\",\"type\":\"string\",\"input\":\"select\",\"operator\":\"is_null\",\"value\":null},{\"id\":\"request_host\",\"field\":\"request_host\",\"type\":\"string\",\"input\":\"text\",\"operator\":\"begins_with\",\"value\":\"example.com\"}]}", + }.merge(expected_tz_param), fragment_uri.query_values) + end + + def test_logs + admin_login + visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-15&end_at=2015-01-18&query=%7B%22condition%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22id%22%3A%22gatekeeper_denied_code%22%2C%22field%22%3A%22gatekeeper_denied_code%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22select%22%2C%22operator%22%3A%22is_null%22%2C%22value%22%3Anull%7D%2C%7B%22id%22%3A%22request_host%22%2C%22field%22%3A%22request_host%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22text%22%2C%22operator%22%3A%22begins_with%22%2C%22value%22%3A%22example.com%22%7D%5D%7D&interval=hour&beta_analytics=false®ion=US" + assert_link("Download CSV", :href => /start_at=2015-01-15/) + + uri = Addressable::URI.parse(page.current_url) + assert_equal("/admin/", uri.path) + assert(uri.fragment) + + fragment_uri = Addressable::URI.parse(uri.fragment) + assert_equal("/stats/logs", fragment_uri.path) + assert_equal({ + "start_at" => "2015-01-15", + "end_at" => "2015-01-18", + "interval" => "hour", + "query" => "{\"condition\":\"AND\",\"rules\":[{\"id\":\"gatekeeper_denied_code\",\"field\":\"gatekeeper_denied_code\",\"type\":\"string\",\"input\":\"select\",\"operator\":\"is_null\",\"value\":null},{\"id\":\"request_host\",\"field\":\"request_host\",\"type\":\"string\",\"input\":\"text\",\"operator\":\"begins_with\",\"value\":\"example.com\"}]}", + }.merge(expected_tz_param), fragment_uri.query_values) + end + + def test_users + admin_login + visit "/admin/#/stats/users/tz=America%2FDenver&search=&start_at=2015-01-15&end_at=2015-01-18&query=%7B%22condition%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22id%22%3A%22gatekeeper_denied_code%22%2C%22field%22%3A%22gatekeeper_denied_code%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22select%22%2C%22operator%22%3A%22is_null%22%2C%22value%22%3Anull%7D%2C%7B%22id%22%3A%22request_host%22%2C%22field%22%3A%22request_host%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22text%22%2C%22operator%22%3A%22begins_with%22%2C%22value%22%3A%22example.com%22%7D%5D%7D&interval=hour&beta_analytics=false®ion=US" + assert_link("Download CSV", :href => /start_at=2015-01-15/) + + uri = Addressable::URI.parse(page.current_url) + assert_equal("/admin/", uri.path) + assert(uri.fragment) + + fragment_uri = Addressable::URI.parse(uri.fragment) + assert_equal("/stats/users", fragment_uri.path) + assert_equal({ + "start_at" => "2015-01-15", + "end_at" => "2015-01-18", + "query" => "{\"condition\":\"AND\",\"rules\":[{\"id\":\"gatekeeper_denied_code\",\"field\":\"gatekeeper_denied_code\",\"type\":\"string\",\"input\":\"select\",\"operator\":\"is_null\",\"value\":null},{\"id\":\"request_host\",\"field\":\"request_host\",\"type\":\"string\",\"input\":\"text\",\"operator\":\"begins_with\",\"value\":\"example.com\"}]}", + }.merge(expected_tz_param), fragment_uri.query_values) + end + + def test_map + admin_login + visit "/admin/#/stats/map/tz=America%2FDenver&search=&start_at=2015-01-15&end_at=2015-01-18&query=%7B%22condition%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22id%22%3A%22gatekeeper_denied_code%22%2C%22field%22%3A%22gatekeeper_denied_code%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22select%22%2C%22operator%22%3A%22is_null%22%2C%22value%22%3Anull%7D%2C%7B%22id%22%3A%22request_host%22%2C%22field%22%3A%22request_host%22%2C%22type%22%3A%22string%22%2C%22input%22%3A%22text%22%2C%22operator%22%3A%22begins_with%22%2C%22value%22%3A%22example.com%22%7D%5D%7D&interval=hour&beta_analytics=false®ion=US" + assert_link("Download CSV", :href => /start_at=2015-01-15/) + + uri = Addressable::URI.parse(page.current_url) + assert_equal("/admin/", uri.path) + assert(uri.fragment) + + fragment_uri = Addressable::URI.parse(uri.fragment) + assert_equal("/stats/map", fragment_uri.path) + assert_equal({ + "start_at" => "2015-01-15", + "end_at" => "2015-01-18", + "query" => "{\"condition\":\"AND\",\"rules\":[{\"id\":\"gatekeeper_denied_code\",\"field\":\"gatekeeper_denied_code\",\"type\":\"string\",\"input\":\"select\",\"operator\":\"is_null\",\"value\":null},{\"id\":\"request_host\",\"field\":\"request_host\",\"type\":\"string\",\"input\":\"text\",\"operator\":\"begins_with\",\"value\":\"example.com\"}]}", + "region" => "US", + }.merge(expected_tz_param), fragment_uri.query_values) + end + + private + + def expected_tz_param + # In all our redirect tests, we pass in "America/Denver" for the timezone + # parameter. If the user's current timezone happens to be America/Denver, + # then the "tz" parameter won't be in the redirect (since Ember doesn't add + # default parameters into the URL). However, if we're running in any other + # timezone, this non-default "tz" parameter should be part of the redirect. + if(ENV["TZ"] == "America/Denver") + {} + else + { "tz" => "America/Denver" } + end + end +end diff --git a/test/admin_ui/test_locales.rb b/test/admin_ui/test_locales.rb new file mode 100644 index 000000000..11ed6bd58 --- /dev/null +++ b/test/admin_ui/test_locales.rb @@ -0,0 +1,87 @@ +require_relative "../test_helper" + +locales_root_dir = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/web-app/config/locales") +I18n.load_path = Dir[File.join(locales_root_dir, "*.yml")] +I18n.backend.load_translations + +class Test::AdminUi::TestLocales < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + # Test all the available locales except the special test "zy" (which we use + # to test for incomplete data). + valid_locales = I18n.available_locales - [:zy] + valid_locales.each do |locale| + locale_method_name = locale.to_s.downcase.gsub(/[^\w]/, "_") + + define_method("test_server_side_translations_in_#{locale_method_name}_locale") do + page.driver.add_headers("Accept-Language" => locale.to_s) + visit "/admin/login" + refute_empty(I18n.t("omniauth_providers.developer", :locale => locale)) + assert_text(I18n.t("omniauth_providers.developer", :locale => locale)) + if(locale != :en) + refute_empty(I18n.t("omniauth_providers.developer", :locale => :en)) + refute_text(I18n.t("omniauth_providers.developer", :locale => :en)) + end + end + + define_method("test_client_side_translations_in_#{locale_method_name}_locale") do + page.driver.add_headers("Accept-Language" => locale.to_s) + admin_login + visit "/admin/#/api_users/new" + + # Form + refute_empty(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => locale)) + assert_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => locale)) + if(locale != :en) + refute_empty(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) + refute_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) + end + + # Navigation + refute_empty(I18n.t("admin.nav.analytics", :locale => locale)) + assert_text(I18n.t("admin.nav.analytics", :locale => locale)) + end + end + + def test_server_side_fall_back_to_english_for_unknown_locale + page.driver.add_headers("Accept-Language" => "zz") + visit "/admin/login" + assert_raises I18n::InvalidLocale do + I18n.t("omniauth_providers.developer", :locale => :zz) + end + refute_empty(I18n.t("omniauth_providers.developer", :locale => :en)) + assert_text(I18n.t("omniauth_providers.developer", :locale => :en)) + end + + def test_client_side_fall_back_to_english_for_unknown_locale + page.driver.add_headers("Accept-Language" => "zz") + admin_login + visit "/admin/#/api_users/new" + assert_raises I18n::InvalidLocale do + I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :zz) + end + refute_empty(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) + assert_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) + end + + def test_server_side_fall_back_to_english_for_missing_data_in_known_locale + page.driver.add_headers("Accept-Language" => "zy") + visit "/admin/login" + assert_equal("translation missing: zy.omniauth_providers.developer", I18n.t("omniauth_providers.developer", :locale => :zy)) + assert_text(I18n.t("omniauth_providers.developer", :locale => :en)) + end + + def test_client_side_fall_back_to_english_for_missing_data_in_known_locale + page.driver.add_headers("Accept-Language" => "zy") + admin_login + visit "/admin/#/api_users/new" + assert_equal("translation missing: zy.mongoid.attributes.api/settings.allowed_ips", I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :zy)) + assert_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) + end +end diff --git a/test/admin_ui/test_login.rb b/test/admin_ui/test_login.rb new file mode 100644 index 000000000..2585dc689 --- /dev/null +++ b/test/admin_ui/test_login.rb @@ -0,0 +1,213 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestLogin < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::DelayServerResponses + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + Admin.where(:registration_source.ne => "seed").delete_all + end + + def test_login_redirects + # Slow down the server side responses to validate the "Loading..." spinner + # shows up (without slowing things down, it periodically goes away too + # quickly for the tests to catch). + delay_server_responses(0.5) do + visit "/admin/" + + # Ensure we get the loading spinner until authentication takes place. + assert_content("Loading...") + + # Navigation should not be visible while loading. + refute_selector("nav") + refute_content("Analytics") + + # Ensure that we eventually get redirected to the login page. + assert_content("Admin Login") + assert_content("Login with") + end + end + + # Since we do some custom things related to the Rails asset path, make sure + # everything is hooked up and the production cache-bused assets are served + # up. + def test_login_assets + visit "/admin/login" + assert_content("Admin Login") + + # Find the stylesheet on the Rails login page, which should have a + # cache-busted URL (note that the href on the page appears to be relative, + # but capybara seems to read it as absolute. That's fine, but noting it in + # case Capybara's future behavior changes). + stylesheet = find("link[rel=stylesheet]", :visible => :hidden) + assert_match(%r{\Ahttps://127\.0\.0\.1:9081/web-assets/admin/login-\w{64}\.css\z}, stylesheet[:href]) + + # Verify that the asset URL can be fetched and returns data. + response = Typhoeus.get(stylesheet[:href], keyless_http_options) + assert_response_code(200, response) + assert_equal("text/css", response.headers["content-type"]) + end + + [ + { + :provider => :facebook, + :login_button_text => "Login with Facebook", + :username_path => "info.email", + :verified_path => "info.verified", + }, + { + :provider => :github, + :login_button_text => "Login with GitHub", + :username_path => "info.email", + :verified_path => "info.email_verified", + }, + { + :provider => :google_oauth2, + :login_button_text => "Login with Google", + :username_path => "info.email", + :verified_path => "extra.raw_info.email_verified", + }, + { + :provider => :ldap, + :login_button_text => "Login with LDAP", + :username_path => "extra.raw_info.sAMAccountName", + }, + { + :provider => :cas, + :login_button_text => "Login with MAX.gov", + :username_path => "uid", + }, + { + :provider => :persona, + :login_button_text => "Login with Persona", + :username_path => "info.email", + }, + ].each do |options| + define_method("test_#{options.fetch(:provider)}_valid_admin") do + assert_login_valid_admin(options) + end + + define_method("test_#{options.fetch(:provider)}_case_insensitive_username_admin") do + assert_login_case_insensitive_username_admin(options) + end + + define_method("test_#{options.fetch(:provider)}_nonexistent_admin") do + assert_login_nonexistent_admin(options) + end + + if(options[:verified_path]) + define_method("test_#{options.fetch(:provider)}_unverified_email") do + assert_login_unverified_email_login(options) + end + end + end + + private + + def assert_login_valid_admin(options) + admin = FactoryGirl.create(:admin, :username => "valid@example.com") + omniauth_data = omniauth_base_data(options) + LazyHash.add(omniauth_data, options.fetch(:username_path), admin.username) + + mock_omniauth(omniauth_data) do + assert_login_permitted(options.fetch(:login_button_text), admin) + end + end + + def assert_login_case_insensitive_username_admin(options) + admin = FactoryGirl.create(:admin, :username => "hello@example.com") + omniauth_data = omniauth_base_data(options) + LazyHash.add(omniauth_data, options.fetch(:username_path), "Hello@ExamplE.Com") + + mock_omniauth(omniauth_data) do + assert_login_permitted(options.fetch(:login_button_text), admin) + end + end + + def assert_login_nonexistent_admin(options) + omniauth_data = omniauth_base_data(options) + LazyHash.add(omniauth_data, options.fetch(:username_path), "noadmin@example.com") + + mock_omniauth(omniauth_data) do + assert_login_forbidden(options.fetch(:login_button_text)) + end + end + + def assert_login_unverified_email_login(options) + admin = FactoryGirl.create(:admin, :username => "unverified@example.com") + omniauth_data = omniauth_base_data(options) + LazyHash.add(omniauth_data, options.fetch(:username_path), admin.username) + LazyHash.add(omniauth_data, options.fetch(:verified_path), false) + + mock_omniauth(omniauth_data) do + assert_login_forbidden(options.fetch(:login_button_text)) + end + end + + def assert_login_permitted(login_button_text, admin) + visit "/admin/" + trigger_click_link(login_button_text) + assert_link("my_account_nav_link", :href => /#{admin.id}/, :visible => :all) + end + + def assert_login_forbidden(login_button_text) + visit "/admin/" + trigger_click_link(login_button_text) + assert_text("not authorized") + refute_link("my_account_nav_link") + end + + def omniauth_base_data(options) + omniauth_base_data = LazyHash.build_hash + omniauth_base_data["provider"] = options.fetch(:provider).to_s + if(options[:verified_path]) + LazyHash.add(omniauth_base_data, options.fetch(:verified_path), true) + end + + omniauth_base_data + end + + def mock_omniauth(omniauth_data) + # Reset the session and clear caches before setting our cookie. For some + # reason this seems necessary to ensure click_link always works correctly + # (otherwise, we sporadically get failures caused by the click_link on the + # login buttons not actually going anywhere). + # + # Possibly related: + # https://github.com/teampoltergeist/poltergeist/issues/814#issuecomment-248830334 + Capybara.reset_session! + page.driver.clear_memory_cache + + # Set a cookie to mock the OmniAuth responses. This relies on the + # TestMockOmniauth middleware we install into the Rails app during the test + # environment. This gives us a way to mock this data from outside the Rails + # test suite. + page.driver.set_cookie("test_mock_omniauth", Base64.urlsafe_encode64(MultiJson.dump(omniauth_data))) + yield + ensure + page.driver.remove_cookie("test_mock_omniauth") + end + + # When using "click_link" on the login buttons we rarely/sporadically see it + # fail to do anything. Capybara doesn't raise an error, so it thinks it's + # clicked the button, but nothing appears to happen. + # + # As a workaround, find the element and programmatically trigger a click + # event on it, which seems to be more reliable. + # + # See: https://github.com/teampoltergeist/poltergeist/issues/530 + # + # I think we've only seen this issue in these tests (and not in other parts + # of the admin app). My theory is that this might be due to the click event + # firing right as the stylesheets load, so the original location it + # calculated and then clicks ends up being incorrect once the stylesheets + # load. I'm not sure about this, but it might explain why it's only happening + # here, and not within the app (since within the app, all the javascript and + # stylesheets must be loaded first for there to be anything rendering on the + # page). + def trigger_click_link(selector) + find_link(selector).trigger("click") + end +end diff --git a/test/admin_ui/test_noscript.rb b/test/admin_ui/test_noscript.rb new file mode 100644 index 000000000..a613d6711 --- /dev/null +++ b/test/admin_ui/test_noscript.rb @@ -0,0 +1,41 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestNoscript < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::DelayServerResponses + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + end + + def test_noscript_message + response = Typhoeus.get("https://127.0.0.1:9081/admin/", keyless_http_options) + assert_response_code(200, response) + assert_equal("text/html", response.headers["content-type"]) + + doc = Nokogiri::HTML(response.body) + + # Ensure there's a