├── .bundler-audit.yml ├── .dockerignore ├── .env.dev ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── cd.yaml │ ├── ci.yaml │ ├── codeql-analysis.yml │ ├── sbom.yaml │ └── vuln-scan.yaml ├── .gitignore ├── .prettierignore ├── .rspec ├── .rubocop.yml ├── .stylelintignore ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Makefile ├── README.md ├── Rakefile ├── SECURITY.md ├── app ├── assets │ ├── images │ │ └── img-favicon.png │ ├── javascripts │ │ ├── application.js │ │ ├── components │ │ │ ├── app.es6 │ │ │ ├── behavior-populate-dropdown.es6 │ │ │ ├── component.es6 │ │ │ ├── orderable-behavior.es6 │ │ │ └── waiting-bar.es6 │ │ ├── main.es6 │ │ └── vendor.js │ └── stylesheets │ │ ├── _base.scss │ │ ├── application.scss │ │ ├── bootstrap-theme.css │ │ └── components │ │ ├── _applications_list.scss │ │ ├── _behaviors_list.scss │ │ ├── _error_messages.scss │ │ ├── _homepage.scss │ │ └── _waiting_bar.scss ├── controllers │ ├── api │ │ ├── application_controller.rb │ │ ├── behaviors_controller.rb │ │ └── errors_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ └── web │ │ ├── application_controller.rb │ │ ├── applications_controller.rb │ │ ├── behaviors_controller.rb │ │ ├── errors_controller.rb │ │ ├── home_controller.rb │ │ ├── memberships_controller.rb │ │ ├── organizations_controller.rb │ │ ├── passwords_controller.rb │ │ ├── projects_controller.rb │ │ ├── sessions_controller.rb │ │ └── users_controller.rb ├── helpers │ ├── devise_helper.rb │ ├── error_helper.rb │ ├── html_helper.rb │ └── mailer_helper.rb ├── mailers │ ├── application_mailer.rb │ ├── devise_mailer.rb │ └── user_mailer.rb ├── models │ ├── .keep │ ├── application.rb │ ├── application_record.rb │ ├── behavior.rb │ ├── concerns │ │ ├── .keep │ │ ├── keyable.rb │ │ └── welcomeable.rb │ ├── membership.rb │ ├── organization.rb │ ├── project.rb │ └── user.rb ├── presenters │ ├── application_presenter.rb │ ├── behavior_presenter.rb │ ├── membership_presenter.rb │ ├── organization_presenter.rb │ ├── project_presenter.rb │ └── user_presenter.rb ├── services │ ├── ability.rb │ ├── behavior_dispatcher.rb │ └── behavior_sorter.rb ├── tasks │ └── sample_data.rake ├── utilities │ ├── active_record_configuration_override.rb │ ├── asset_host.rb │ └── boolean_environment_variable.rb ├── validators │ ├── email_validator.rb │ └── version_validator.rb └── views │ ├── devise │ ├── mailer │ │ └── reset_password_instructions.en.html.erb │ ├── passwords │ │ ├── edit.html.erb │ │ └── new.html.erb │ └── sessions │ │ └── new.html.erb │ ├── layouts │ ├── application.html.erb │ └── mailer.html.erb │ ├── shared │ ├── _error_messages.html.erb │ ├── _exception_backtrace.html.erb │ ├── _flashes.html.erb │ └── _logo.html.erb │ ├── user_mailer │ └── welcome_email.en.html.erb │ └── web │ ├── applications │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ ├── behaviors │ ├── _form.html.erb │ ├── _mini.html.erb │ ├── edit.html.erb │ └── new.html.erb │ ├── errors │ ├── forbidden.html.erb │ ├── internal_server_error.html.erb │ └── not_found.html.erb │ ├── home │ └── show.html.erb │ ├── memberships │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ └── new.html.erb │ ├── organizations │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ └── new.html.erb │ ├── projects │ ├── _form.html.erb │ ├── edit.html.erb │ ├── new.html.erb │ └── show.html.erb │ └── users │ ├── _form.html.erb │ └── edit.html.erb ├── bin ├── bundle ├── rails ├── rake ├── rspec └── spring ├── config.ru ├── config ├── application.rb ├── boot.rb ├── brakeman.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── camaraderie.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── devise.rb │ ├── devise_security.rb │ ├── filter_parameter_logging.rb │ ├── friendly_id.rb │ ├── gaffe.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── mini_check.rb │ ├── new_framework_defaults_5_2.rb │ ├── sentry.rb │ ├── session_store.rb │ ├── sprockets_es6.rb │ └── wrap_parameters.rb ├── locales │ ├── activerecord.en.yml │ ├── applications.en.yml │ ├── behaviors.en.yml │ ├── devise.en.yml │ ├── devise.security_extension.en.yml │ ├── en.yml │ ├── errors.en.yml │ ├── home.en.yml │ ├── layouts.en.yml │ ├── memberships.en.yml │ ├── organizations.en.yml │ ├── passwords.en.yml │ ├── projects.en.yml │ ├── sessions.en.yml │ ├── shared.en.yml │ ├── user_mailer.en.yml │ └── users.en.yml ├── puma.rb ├── routes.rb ├── schemas │ └── behavior_data.jsonschema ├── secrets.yml ├── spring.rb └── storage.yml ├── db ├── migrate │ ├── 20131108183954_create_applications.rb │ ├── 20131108184040_create_projects.rb │ ├── 20131108184334_add_project_key.rb │ ├── 20131108191020_rename_project_app_id.rb │ ├── 20131112125557_create_friendly_id_slugs.rb │ ├── 20131112125627_add_slugs_to_applications_and_projects.rb │ ├── 20131112140005_create_behaviors.rb │ ├── 20131112194936_add_sort_order_to_behaviors.rb │ ├── 20131119114353_devise_create_users.rb │ ├── 20131119155530_add_slug_to_users.rb │ ├── 20131120155642_add_deleted_at_to_models.rb │ ├── 20131224183944_remove_unique_index_on_applications.rb │ ├── 20131224184148_remove_a_lot_of_indexes.rb │ ├── 20131224184526_create_organizations.rb │ ├── 20131230183006_add_camaraderie.rb │ ├── 20131231152100_add_recoverable_fields_to_users.rb │ ├── 20140219181729_create_versions.rb │ ├── 20140220160947_rename_behavior_version_to_version_number.rb │ ├── 20141223195846_add_is_super_admin_to_organizations.rb │ ├── 20150610135602_add_organization_cache_counters.rb │ ├── 20180420163300_add_datetime_to_behaviors.rb │ ├── 20250314190743_add_old_passwords_table.rb │ └── 20250403130915_add_unique_session_id_to_users.rb └── schema.rb ├── package-lock.json ├── package.json ├── prettier.config.js ├── scripts └── docker-entrypoint.sh ├── spec ├── factories │ ├── applications.rb │ ├── behaviors.rb │ ├── organizations.rb │ ├── projects.rb │ └── users.rb ├── models │ ├── application_spec.rb │ ├── behavior_spec.rb │ ├── organization_spec.rb │ ├── project_spec.rb │ └── user_spec.rb ├── requests │ └── api │ │ └── behaviors_requests_spec.rb ├── services │ ├── behavior_dispatcher_spec.rb │ └── behavior_sorter_spec.rb └── spec_helper.rb ├── stylelint.config.js └── vendor └── assets ├── fonts ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf └── fontawesome-webfont.woff └── stylesheets └── font-awesome.css.erb /.bundler-audit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ignore: 3 | - CVE-2024-27456 # https://github.com/advisories/GHSA-785g-282q-pwvx (packaging issue with rack-cors) 4 | - CVE-2024-54133 # https://github.com/rails/rails/security/advisories/GHSA-vfm5-rmrh-j26v (We don’t generate CSP from user input) 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | log/ 3 | .git/ 4 | node_modules/ 5 | spec 6 | .* 7 | *.md 8 | Dockerfile 9 | Makefile 10 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | # The environment for Rails to use 2 | RACK_ENV=development 3 | 4 | # The port on which we want to run the server 5 | PORT=3000 6 | 7 | # The canonical host and port to build URLs in emails 8 | CANONICAL_HOST=localhost 9 | CANONICAL_PORT=3000 10 | 11 | # Require HTTPS requests 12 | FORCE_SSL=false 13 | 14 | # Database URL 15 | DATABASE_URL=postgres://localhost/killswitch_development 16 | 17 | # Secret key (use `rake secret` to generate one) 18 | SECRET_KEY_BASE= 19 | 20 | # Mailer defaults 21 | # MAILER_FROM= 22 | 23 | # SMTP settings 24 | # SMTP_ADDRESS= 25 | # SMTP_PORT= 26 | # SMTP_USERNAME= 27 | # SMTP_PASSWORD= 28 | 29 | # Sentry DSN for error reporting 30 | # SENTRY_DSN= 31 | 32 | # Show backtrace on error pages 33 | SHOW_BACKTRACE=1 34 | 35 | # Require a Basic Auth username and password 36 | # BASIC_AUTH_USERNAME= 37 | # BASIC_AUTH_PASSWORD= 38 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # The environment for Rails to use 2 | RACK_ENV=test 3 | 4 | # The canonical host and port to build URLs in emails 5 | CANONICAL_HOST=localhost 6 | 7 | # Mailer defaults 8 | MAILER_FROM=localhost 9 | 10 | # Database URL 11 | DATABASE_URL=postgres://postgres:postgres@localhost/killswitch_test 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | vendor/bundle/* 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | es2021: true 6 | }, 7 | plugins: ['mirego'], 8 | extends: ['plugin:mirego/recommended'], 9 | globals: { 10 | $: 'readonly', 11 | require: 'readonly', 12 | sortable: 'readonly' 13 | }, 14 | rules: { 15 | 'operator-linebreak': ['error', 'after'] 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | vendor/assets/javascripts/* -crlf -diff 2 | vendor/assets/stylesheets/* -crlf -diff 3 | vendor/assets/fonts/* -crlf -diff 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "docker" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📖 Description and motivation 2 | 3 | 4 | 5 | ## 👷 Work done 6 | 7 | #### Tasks 8 | 9 | - [x] Task 1 10 | - [ ] Task 2 11 | 12 | #### Additional notes 13 | 14 | 15 | 16 | ## 🎉 Result 17 | 18 | 19 | 20 | ## 🦀 Dispatch 21 | 22 | `#dispatch/rails` 23 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: false 13 | 14 | jobs: 15 | docker-tag: 16 | runs-on: ubuntu-24.04 17 | 18 | outputs: 19 | docker-tag: ${{steps.tag.outputs.tag}} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - id: tag 25 | run: | 26 | TAG=$(echo "${{github.ref}}" | sed -e 's,.*/\(.*\),\1,') 27 | [[ "${{github.ref}}" == "refs/tags/"* ]] && TAG=$(echo $TAG | sed -e 's/^v//') 28 | [ "$TAG" == "main" ] && TAG=latest 29 | echo "tag=$TAG" >> $GITHUB_OUTPUT 30 | 31 | build-and-push: 32 | if: ${{ github.actor != 'dependabot[bot]' }} 33 | needs: docker-tag 34 | runs-on: ubuntu-24.04 35 | env: 36 | DOCKER_TAG: ${{needs.docker-tag.outputs.docker-tag}} 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | 41 | - run: make build 42 | 43 | - uses: docker/login-action@v2 44 | with: 45 | registry: ghcr.io 46 | username: mirego-builds 47 | password: ${{secrets.MIREGO_GITHUB_PACKAGES_ACCESS_TOKEN}} 48 | 49 | - run: make push 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-24.04 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | services: 20 | db: 21 | image: postgres:10.19 22 | env: 23 | POSTGRES_DB: killswitch_test 24 | POSTGRES_USER: postgres 25 | POSTGRES_PASSWORD: postgres 26 | ports: ['5432:5432'] 27 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 28 | 29 | env: 30 | CANONICAL_HOST: localhost 31 | DATABASE_URL: postgres://postgres:postgres@localhost/killswitch_test 32 | MAILER_FROM: localhost 33 | RAILS_ENV: test 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | 38 | - uses: actions/setup-node@v3 39 | with: 40 | node-version: 16 41 | cache: npm 42 | 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | bundler-cache: true 46 | 47 | - run: npm install 48 | - run: make check 49 | - run: make lint 50 | - run: make test 51 | - run: make build 52 | 53 | - uses: aquasecurity/trivy-action@master 54 | with: 55 | scan-type: 'config' 56 | ignore-unfixed: true 57 | format: table 58 | severity: 'CRITICAL,HIGH' 59 | skip-dirs: vendor,node_modules 60 | exit-code: 1 61 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | schedule: 11 | - cron: '0 13 * * 1' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-24.04 21 | permissions: 22 | actions: read 23 | contents: read 24 | security-events: write 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | language: 30 | - javascript 31 | - ruby 32 | 33 | steps: 34 | - uses: actions/checkout@v3 35 | 36 | - uses: github/codeql-action/init@v3 37 | with: 38 | languages: ${{ matrix.language }} 39 | 40 | - uses: github/codeql-action/analyze@v3 41 | -------------------------------------------------------------------------------- /.github/workflows/sbom.yaml: -------------------------------------------------------------------------------- 1 | name: SBOM 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | upload-sbom: 14 | runs-on: ubuntu-24.04 15 | permissions: 16 | contents: read 17 | security-events: write 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - run: make build 23 | env: 24 | DOCKER_TAG: sbom-snapshot 25 | 26 | - uses: anchore/sbom-action@latest 27 | with: 28 | image: mirego/killswitch:sbom-snapshot 29 | dependency-snapshot: true 30 | -------------------------------------------------------------------------------- /.github/workflows/vuln-scan.yaml: -------------------------------------------------------------------------------- 1 | name: Vulnerability scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | schedule: 11 | - cron: '30 13 * * 3' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | vuln-scan: 19 | runs-on: ubuntu-24.04 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - run: make build 25 | 26 | - uses: aquasecurity/trivy-action@master 27 | with: 28 | image-ref: mirego/killswitch:${{ github.sha }} 29 | ignore-unfixed: true 30 | format: sarif 31 | output: vuln-scan-results.sarif 32 | vuln-type: os,library 33 | 34 | - uses: github/codeql-action/upload-sarif@v2 35 | if: ${{ github.actor != 'dependabot[bot]' }} 36 | with: 37 | sarif_file: vuln-scan-results.sarif 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore bundler config. 2 | /.bundle 3 | 4 | # Ignore all logfiles and tempfiles. 5 | /log/*.log 6 | /tmp 7 | 8 | # NPM 9 | /node_modules 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | vendor/bundle 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: rubocop-rails 2 | 3 | AllCops: 4 | Include: 5 | - Rakefile 6 | - config.ru 7 | - Gemfile 8 | - '**/*.rb' 9 | Exclude: 10 | - bin/** 11 | - db/schema.rb 12 | - db/migrate/** 13 | - script/** 14 | - vendor/**/* 15 | - node_modules/**/* 16 | 17 | Style/Documentation: 18 | Enabled: false 19 | 20 | Style/Encoding: 21 | Enabled: false 22 | 23 | Layout/LineLength: 24 | Max: 200 25 | 26 | Layout/AccessModifierIndentation: 27 | EnforcedStyle: outdent 28 | 29 | Layout/CaseIndentation: 30 | EnforcedStyle: case 31 | IndentOneStep: true 32 | 33 | Metrics/MethodLength: 34 | CountComments: false 35 | Max: 20 36 | 37 | Style/SignalException: 38 | Enabled: false 39 | 40 | Style/ColonMethodCall: 41 | Enabled: false 42 | 43 | Style/AsciiComments: 44 | Enabled: false 45 | 46 | Style/RegexpLiteral: 47 | Enabled: false 48 | 49 | Lint/AssignmentInCondition: 50 | Enabled: false 51 | 52 | Metrics/ParameterLists: 53 | CountKeywordArgs: false 54 | 55 | Style/SingleLineBlockParams: 56 | Methods: 57 | - reduce: 58 | - memo 59 | - item 60 | 61 | Metrics/AbcSize: 62 | Enabled: false 63 | 64 | Style/CollectionMethods: 65 | Enabled: true 66 | 67 | Style/SymbolArray: 68 | Enabled: false 69 | 70 | Layout/ExtraSpacing: 71 | Enabled: true 72 | 73 | Style/Lambda: 74 | Enabled: false 75 | 76 | Style/ClassAndModuleChildren: 77 | Enabled: false 78 | 79 | Style/Sample: 80 | Enabled: false 81 | 82 | Style/PercentLiteralDelimiters: 83 | PreferredDelimiters: 84 | default: '()' 85 | '%w': '()' 86 | '%i': '()' 87 | 88 | Metrics/BlockLength: 89 | Exclude: 90 | - '**/*_spec.rb' 91 | 92 | Style/FrozenStringLiteralComment: 93 | Enabled: false 94 | 95 | Style/ExpandPathArguments: 96 | Enabled: false 97 | 98 | Naming/MemoizedInstanceVariableName: 99 | Enabled: false 100 | 101 | Rails/UniqueValidationWithoutIndex: 102 | Enabled: false 103 | 104 | Gemspec/DeprecatedAttributeAssignment: 105 | Enabled: true 106 | 107 | Gemspec/DevelopmentDependencies: 108 | Enabled: true 109 | 110 | Gemspec/RequireMFA: 111 | Enabled: true 112 | 113 | Layout/LineContinuationLeadingSpace: 114 | Enabled: true 115 | 116 | Layout/LineContinuationSpacing: 117 | Enabled: true 118 | 119 | Layout/LineEndStringConcatenationIndentation: 120 | Enabled: true 121 | 122 | Layout/SpaceBeforeBrackets: 123 | Enabled: true 124 | 125 | Lint/AmbiguousAssignment: 126 | Enabled: true 127 | 128 | Lint/AmbiguousOperatorPrecedence: 129 | Enabled: true 130 | 131 | Lint/AmbiguousRange: 132 | Enabled: true 133 | 134 | Lint/ConstantOverwrittenInRescue: 135 | Enabled: true 136 | 137 | Lint/DeprecatedConstants: 138 | Enabled: true 139 | 140 | Lint/DuplicateBranch: 141 | Enabled: true 142 | 143 | Lint/DuplicateMagicComment: 144 | Enabled: true 145 | 146 | Lint/DuplicateRegexpCharacterClassElement: 147 | Enabled: true 148 | 149 | Lint/EmptyBlock: 150 | Enabled: true 151 | 152 | Lint/EmptyClass: 153 | Enabled: true 154 | 155 | Lint/EmptyInPattern: 156 | Enabled: true 157 | 158 | Lint/IncompatibleIoSelectWithFiberScheduler: 159 | Enabled: true 160 | 161 | Lint/LambdaWithoutLiteralBlock: 162 | Enabled: true 163 | 164 | Lint/NoReturnInBeginEndBlocks: 165 | Enabled: true 166 | 167 | Lint/NonAtomicFileOperation: 168 | Enabled: true 169 | 170 | Lint/NumberedParameterAssignment: 171 | Enabled: true 172 | 173 | Lint/OrAssignmentToConstant: 174 | Enabled: true 175 | 176 | Lint/RedundantDirGlobSort: 177 | Enabled: true 178 | 179 | Lint/RefinementImportMethods: 180 | Enabled: true 181 | 182 | Lint/RequireRangeParentheses: 183 | Enabled: true 184 | 185 | Lint/RequireRelativeSelfPath: 186 | Enabled: true 187 | 188 | Lint/SymbolConversion: 189 | Enabled: true 190 | 191 | Lint/ToEnumArguments: 192 | Enabled: true 193 | 194 | Lint/TripleQuotes: 195 | Enabled: true 196 | 197 | Lint/UnexpectedBlockArity: 198 | Enabled: true 199 | 200 | Lint/UnmodifiedReduceAccumulator: 201 | Enabled: true 202 | 203 | Lint/UselessRescue: 204 | Enabled: true 205 | 206 | Lint/UselessRuby2Keywords: 207 | Enabled: true 208 | 209 | Naming/BlockForwarding: 210 | Enabled: true 211 | 212 | Security/CompoundHash: 213 | Enabled: true 214 | 215 | Security/IoMethods: 216 | Enabled: true 217 | 218 | Style/ArgumentsForwarding: 219 | Enabled: true 220 | 221 | Style/ArrayIntersect: 222 | Enabled: true 223 | 224 | Style/CollectionCompact: 225 | Enabled: true 226 | 227 | Style/ComparableClamp: 228 | Enabled: true 229 | 230 | Style/ConcatArrayLiterals: 231 | Enabled: true 232 | 233 | Style/DocumentDynamicEvalDefinition: 234 | Enabled: true 235 | 236 | Style/EmptyHeredoc: 237 | Enabled: true 238 | 239 | Style/EndlessMethod: 240 | Enabled: true 241 | 242 | Style/EnvHome: 243 | Enabled: true 244 | 245 | Style/FetchEnvVar: 246 | Enabled: true 247 | 248 | Style/FileRead: 249 | Enabled: true 250 | 251 | Style/FileWrite: 252 | Enabled: true 253 | 254 | Style/HashConversion: 255 | Enabled: true 256 | 257 | Style/HashExcept: 258 | Enabled: true 259 | 260 | Style/IfWithBooleanLiteralBranches: 261 | Enabled: true 262 | 263 | Style/InPatternThen: 264 | Enabled: true 265 | 266 | Style/MagicCommentFormat: 267 | Enabled: true 268 | 269 | Style/MapCompactWithConditionalBlock: 270 | Enabled: true 271 | 272 | Style/MapToHash: 273 | Enabled: true 274 | 275 | Style/MapToSet: 276 | Enabled: true 277 | 278 | Style/MinMaxComparison: 279 | Enabled: true 280 | 281 | Style/MultilineInPatternThen: 282 | Enabled: true 283 | 284 | Style/NegatedIfElseCondition: 285 | Enabled: true 286 | 287 | Style/NestedFileDirname: 288 | Enabled: true 289 | 290 | Style/NilLambda: 291 | Enabled: true 292 | 293 | Style/NumberedParameters: 294 | Enabled: true 295 | 296 | Style/NumberedParametersLimit: 297 | Enabled: true 298 | 299 | Style/ObjectThen: 300 | Enabled: true 301 | 302 | Style/OpenStructUse: 303 | Enabled: true 304 | 305 | Style/OperatorMethodCall: 306 | Enabled: true 307 | 308 | Style/QuotedSymbols: 309 | Enabled: true 310 | 311 | Style/RedundantArgument: 312 | Enabled: true 313 | 314 | Style/RedundantConstantBase: 315 | Enabled: true 316 | 317 | Style/RedundantDoubleSplatHashBraces: 318 | Enabled: true 319 | 320 | Style/RedundantEach: 321 | Enabled: true 322 | 323 | Style/RedundantHeredocDelimiterQuotes: 324 | Enabled: true 325 | 326 | Style/RedundantInitialize: 327 | Enabled: true 328 | 329 | Style/RedundantSelfAssignmentBranch: 330 | Enabled: true 331 | 332 | Style/RedundantStringEscape: 333 | Enabled: true 334 | 335 | Style/SelectByRegexp: 336 | Enabled: true 337 | 338 | Style/StringChars: 339 | Enabled: true 340 | 341 | Style/SwapValues: 342 | Enabled: true 343 | 344 | Rails/ActionControllerFlashBeforeRender: 345 | Enabled: true 346 | 347 | Rails/ActionControllerTestCase: 348 | Enabled: true 349 | 350 | Rails/ActionOrder: 351 | Enabled: true 352 | 353 | Rails/ActiveRecordCallbacksOrder: 354 | Enabled: true 355 | 356 | Rails/ActiveSupportOnLoad: 357 | Enabled: true 358 | 359 | Rails/AddColumnIndex: 360 | Enabled: true 361 | 362 | Rails/AfterCommitOverride: 363 | Enabled: true 364 | 365 | Rails/AttributeDefaultBlockValue: 366 | Enabled: true 367 | 368 | Rails/CompactBlank: 369 | Enabled: true 370 | 371 | Rails/DeprecatedActiveModelErrorsMethods: 372 | Enabled: true 373 | 374 | Rails/DotSeparatedKeys: 375 | Enabled: true 376 | 377 | Rails/DuplicateAssociation: 378 | Enabled: true 379 | 380 | Rails/DuplicateScope: 381 | Enabled: true 382 | 383 | Rails/DurationArithmetic: 384 | Enabled: true 385 | 386 | Rails/EagerEvaluationLogMessage: 387 | Enabled: true 388 | 389 | Rails/ExpandedDateRange: 390 | Enabled: true 391 | 392 | Rails/FindById: 393 | Enabled: true 394 | 395 | Rails/FreezeTime: 396 | Enabled: true 397 | 398 | Rails/I18nLazyLookup: 399 | Enabled: true 400 | 401 | Rails/I18nLocaleAssignment: 402 | Enabled: true 403 | 404 | Rails/I18nLocaleTexts: 405 | Enabled: true 406 | 407 | Rails/IgnoredColumnsAssignment: 408 | Enabled: true 409 | 410 | Rails/Inquiry: 411 | Enabled: true 412 | 413 | Rails/MailerName: 414 | Enabled: true 415 | 416 | Rails/MatchRoute: 417 | Enabled: true 418 | 419 | Rails/MigrationClassName: 420 | Enabled: true 421 | 422 | Rails/NegateInclude: 423 | Enabled: true 424 | 425 | Rails/Pluck: 426 | Enabled: true 427 | 428 | Rails/PluckInWhere: 429 | Enabled: true 430 | 431 | Rails/RedundantPresenceValidationOnBelongsTo: 432 | Enabled: true 433 | 434 | Rails/RedundantTravelBack: 435 | Enabled: true 436 | 437 | Rails/RenderInline: 438 | Enabled: true 439 | 440 | Rails/RenderPlainText: 441 | Enabled: true 442 | 443 | Rails/ResponseParsedBody: 444 | Enabled: true 445 | 446 | Rails/RootJoinChain: 447 | Enabled: true 448 | 449 | Rails/RootPathnameMethods: 450 | Enabled: true 451 | 452 | Rails/RootPublicPath: 453 | Enabled: true 454 | 455 | Rails/ShortI18n: 456 | Enabled: true 457 | 458 | Rails/SquishedSQLHeredocs: 459 | Enabled: true 460 | 461 | Rails/StripHeredoc: 462 | Enabled: true 463 | 464 | Rails/TimeZoneAssignment: 465 | Enabled: true 466 | 467 | Rails/ToFormattedS: 468 | Enabled: true 469 | 470 | Rails/ToSWithArgument: 471 | Enabled: true 472 | 473 | Rails/TopLevelHashWithIndifferentAccess: 474 | Enabled: true 475 | 476 | Rails/TransactionExitStatement: 477 | Enabled: true 478 | 479 | Rails/UnusedIgnoredColumns: 480 | Enabled: true 481 | 482 | Rails/WhereEquals: 483 | Enabled: true 484 | 485 | Rails/WhereExists: 486 | Enabled: true 487 | 488 | Rails/WhereMissing: 489 | Enabled: true 490 | 491 | Rails/WhereNot: 492 | Enabled: true 493 | 494 | Rails/WhereNotWithMultipleConditions: 495 | Enabled: true 496 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | vendor/* 3 | tmp/* 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.2.2 2 | nodejs 22.3.0 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [1.2.2] - 2024-01-15 8 | 9 | - Upgrade several Ruby gems ([references](https://github.com/mirego/killswitch/pulls?q=is%3Apr+is%3Amerged+user%3Adependabot+created%3A2023-02-05..2024-01-15)) 10 | 11 | ## [1.2.1] - 2023-02-05 12 | 13 | - Fix bug where release tag would not be signed 14 | 15 | ## [1.2.0] - 2023-02-05 16 | 17 | - Upgrade Ruby to 3.2.0 and other necessary changes/upgrades related to it 18 | 19 | ## [1.1.2] - 2023-01-04 20 | 21 | - Upgrade CanCanCan and FriendlyId 22 | - Handle 403 & 404 errors better 23 | 24 | ## [1.1.1] - 2023-01-04 25 | 26 | - Happy new year! 🎉 27 | - Upgrade a few Ruby gems and NPM packages 28 | 29 | ## [1.1.0] - 2022-11-23 30 | 31 | - Upgrade `ActiveRecord::JSONValidator` 32 | 33 | ## [1.0.9] - 2022-10-21 34 | 35 | - Update logo 36 | 37 | ## [1.0.8] - 2022-10-18 38 | 39 | - Update vulnerable Nokogiri version to 1.13.9 40 | 41 | ## [1.0.7] - 2022-10-18 42 | 43 | - Add message for user without organizations 44 | 45 | ## [1.0.6] - 2022-09-30 46 | 47 | - Add version number on homepage 48 | 49 | ## [1.0.5] - 2022-09-14 50 | 51 | - Upgrade to Rails 6.1.6.1 52 | 53 | ## [1.0.4] - 2022-02-15 54 | 55 | - Add CD workflow to automatically push new versions to GitHub Packages 56 | 57 | ## [1.0.3] - 2022-02-11 58 | 59 | - Upgrade to Rails 6.1.4.6 60 | - Upgrade to Puma 5.6.2 61 | 62 | ## [1.0.2] - 2022-02-08 63 | 64 | - Add better “sample data” script 65 | 66 | ## [1.0.1] - 2022-02-08 67 | 68 | - Upgrade to Ruby 2.7.x 69 | 70 | ## [1.0.0] - 2022-01-28 71 | 72 | - Initial open source release (from 9 year old private repository) 73 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Contact: info@mirego.com 4 | 5 | ## Why have a Code of Conduct? 6 | 7 | As contributors and maintainers of this project, we are committed to providing a friendly, safe and welcoming environment for all, regardless of age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. 8 | 9 | The goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about the project effectively, productively, and respectfully, even in face of disagreements. The Code of Conduct also provides a mechanism for resolving conflicts in the community when they arise. 10 | 11 | ## Our Values 12 | 13 | These are the values Killswitch developers should aspire to: 14 | 15 | - Be friendly and welcoming 16 | - Be patient 17 | - Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) 18 | - Be thoughtful 19 | - Productive communication requires effort. Think about how your words will be interpreted. 20 | - Remember that sometimes it is best to refrain entirely from commenting. 21 | - Be respectful 22 | - In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively. 23 | - Avoid destructive behavior 24 | - Derailing: stay on topic; if you want to talk about something else, start a new conversation. 25 | - Unconstructive criticism: don't merely decry the current state of affairs; offer (or at least solicit) suggestions as to how things may be improved. 26 | - Snarking (pithy, unproductive, sniping comments). 27 | 28 | The following actions are explicitly forbidden: 29 | 30 | - Insulting, demeaning, hateful, or threatening remarks. 31 | - Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. 32 | - Bullying or systematic harassment. 33 | - Unwelcome sexual advances. 34 | - Incitement to any of these. 35 | 36 | ## Acknowledgements 37 | 38 | This document was based on the Code of Conduct from the Elixir project. 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.2.2-alpine3.17 AS base 2 | 3 | # Create and define work directory 4 | WORKDIR /opt/killswitch 5 | 6 | # Install OS dependencies 7 | RUN apk --update --no-cache add nodejs tzdata libpq 8 | 9 | # Install Ruby dependencies 10 | RUN gem update --system 3.4.1 && gem update --system 11 | 12 | FROM base AS build 13 | 14 | # Copy only the minimal required files 15 | COPY . /opt/killswitch/ 16 | 17 | # Update system and install dependencies 18 | RUN apk --update --no-cache add --virtual build-dependencies build-base git nodejs npm postgresql-dev 19 | 20 | # Install Ruby gems 21 | RUN bundle config set --local without 'development test' && bundle install && bundle binstubs --all 22 | 23 | # Install NPM packages 24 | RUN npm set progress=false && npm install --silent --production 25 | 26 | # Precompile assets 27 | RUN SECRET_KEY_BASE=__UNUSED_BUT_REQUIRED__ RAILS_ENV=production bundle exec rake assets:precompile 28 | 29 | FROM base AS release 30 | 31 | # Copy distribution files only 32 | COPY . . 33 | COPY --from=build /usr/local/bundle/ /usr/local/bundle/ 34 | COPY --from=build /opt/killswitch/public/assets/ /opt/killswitch/public/assets/ 35 | 36 | # Copy entrypoint script 37 | COPY scripts/docker-entrypoint.sh /usr/local/bin 38 | RUN chmod a+x /usr/local/bin/docker-entrypoint.sh 39 | 40 | # Create user 41 | RUN adduser -D -h /opt/killswitch -u 5000 killswitch && chown -R killswitch: /opt/killswitch 42 | 43 | USER killswitch 44 | 45 | HEALTHCHECK CMD curl --fail http://localhost:3000 || exit 1 46 | 47 | # Execute entrypoint script 48 | ENTRYPOINT ["docker-entrypoint.sh"] 49 | CMD bundle exec puma -p $PORT -C config/puma.rb 50 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '3.2.2' 4 | 5 | gem 'activerecord_json_validator', '~> 3.1.0' 6 | gem 'autoprefixer-rails', '6.4.0.1' 7 | gem 'bootsnap' 8 | gem 'bourgeois' 9 | gem 'camaraderie' 10 | gem 'cancancan' 11 | gem 'devise', '~> 4.9' 12 | gem 'devise-security', github: 'devise-security/devise-security' 13 | gem 'friendly_id' 14 | gem 'gaffe', '1.2.0' 15 | gem 'jquery-turbolinks' 16 | gem 'mini_check' 17 | gem 'paper_trail', '~> 11.1' 18 | gem 'pg' 19 | gem 'puma' 20 | gem 'rack-accept', require: 'rack/accept' 21 | gem 'rack-canonical-host', '1.3.0' 22 | gem 'rack-cors', require: 'rack/cors' 23 | gem 'rack-ssl', require: 'rack/ssl' 24 | gem 'rails', '6.1.7.10' 25 | gem 'rails-html-sanitizer', '~> 1.6' 26 | gem 'ranked-model' 27 | gem 'rexml', '~> 3.4' 28 | gem 'sass-rails', '~> 5.1' 29 | gem 'sentry-rails' 30 | gem 'sentry-ruby' 31 | gem 'sprockets', '3.7.2' 32 | gem 'sprockets-es6' 33 | gem 'sprockets-rails', '3.5.2' 34 | gem 'turbolinks', '~> 5.2' 35 | gem 'uglifier', '~> 3.2' 36 | gem 'versionomy' 37 | 38 | group :development, :test do 39 | gem 'brakeman' 40 | gem 'bundler-audit' 41 | gem 'database_cleaner-active_record' 42 | gem 'factory_bot_rails' 43 | gem 'ffaker' 44 | gem 'parser', '~> 3.3.8' 45 | gem 'rspec-rails', '~> 6.1' 46 | gem 'rubocop', '~> 1.76' 47 | gem 'rubocop-rails', require: false 48 | end 49 | 50 | group :development do 51 | gem 'spring' 52 | gem 'spring-commands-rspec' 53 | end 54 | 55 | group :production do 56 | gem 'rails_12factor' 57 | end 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2022, Mirego All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of the Mirego nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Build configuration 2 | # ------------------- 3 | 4 | DOCKER_TAG ?= `git rev-parse HEAD` 5 | DOCKER_REGISTRY = ghcr.io 6 | DOCKER_LOCAL_IMAGE = mirego/killswitch:$(DOCKER_TAG) 7 | DOCKER_REMOTE_IMAGE = $(DOCKER_REGISTRY)/$(DOCKER_LOCAL_IMAGE) 8 | 9 | # Linter and formatter configuration 10 | # ---------------------------------- 11 | 12 | PRETTIER_FILES_PATTERN = '*.config.js' '**/*.{js,es6}' './*.md' './*/*.md' 13 | SCRIPTS_PATTERN = '**/*.es6' '**/*.js' 14 | STYLES_PATTERN = '**/*.scss' '**/*.css' 15 | 16 | # Introspection targets 17 | # --------------------- 18 | 19 | .PHONY: help 20 | help: header targets 21 | 22 | .PHONY: header 23 | header: 24 | @echo "\033[34mEnvironment\033[0m" 25 | @echo "\033[34m---------------------------------------------------------------\033[0m" 26 | @printf "\033[33m%-23s\033[0m" "DOCKER_TAG" 27 | @printf "\033[35m%s\033[0m" $(DOCKER_TAG) 28 | @echo "" 29 | @printf "\033[33m%-23s\033[0m" "DOCKER_REGISTRY" 30 | @printf "\033[35m%s\033[0m" $(DOCKER_REGISTRY) 31 | @echo "" 32 | @printf "\033[33m%-23s\033[0m" "DOCKER_LOCAL_IMAGE" 33 | @printf "\033[35m%s\033[0m" $(DOCKER_LOCAL_IMAGE) 34 | @echo "" 35 | @printf "\033[33m%-23s\033[0m" "DOCKER_REMOTE_IMAGE" 36 | @printf "\033[35m%s\033[0m" $(DOCKER_REMOTE_IMAGE) 37 | @echo "\n" 38 | 39 | .PHONY: targets 40 | targets: 41 | @echo "\033[34mTargets\033[0m" 42 | @echo "\033[34m---------------------------------------------------------------\033[0m" 43 | @perl -nle'print $& if m{^[a-zA-Z_-\d]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' 44 | 45 | # Build targets 46 | # ------------- 47 | 48 | .PHONY: build 49 | build: ## Build the Docker image for the OTP release 50 | docker build --rm --tag $(DOCKER_LOCAL_IMAGE) . 51 | 52 | .PHONY: push 53 | push: ## Push the Docker image to the registry 54 | docker tag $(DOCKER_LOCAL_IMAGE) $(DOCKER_REMOTE_IMAGE) 55 | docker push $(DOCKER_REMOTE_IMAGE) 56 | 57 | # Development targets 58 | # ------------------- 59 | 60 | .PHONY: dependencies 61 | dependencies: 62 | bundle install 63 | npm install 64 | 65 | .PHONY: test 66 | test: 67 | RAILS_ENV=test bundle exec rails db:environment:set 68 | RAILS_ENV=test bundle exec rake db:schema:load 69 | RAILS_ENV=test bundle exec rspec 70 | 71 | # Check, lint and format targets 72 | # ------------------------------ 73 | 74 | .PHONY: format 75 | format: ## Format project files 76 | npx prettier --write $(PRETTIER_FILES_PATTERN) 77 | npx eslint $(SCRIPTS_PATTERN) --fix --quiet 78 | npx stylelint $(STYLES_PATTERN) --fix --quiet 79 | 80 | .PHONY: lint 81 | lint: lint-format lint-ruby lint-scripts lint-styles ## Lint project files 82 | 83 | .PHONY: lint-format 84 | lint-format: 85 | npx prettier --check $(PRETTIER_FILES_PATTERN) 86 | 87 | .PHONY: lint-ruby 88 | lint-ruby: 89 | bundle exec rubocop 90 | 91 | .PHONY: lint-scripts 92 | lint-scripts: 93 | npx eslint $(SCRIPTS_PATTERN) 94 | 95 | .PHONY: lint-styles 96 | lint-styles: 97 | npx stylelint $(STYLES_PATTERN) 98 | 99 | .PHONY: check 100 | check: check-dependencies-security check-code-security 101 | 102 | .PHONY: check-dependencies-security 103 | check-dependencies-security: 104 | bundle exec bundle-audit check --update 105 | npm audit --production --audit-level=critical 106 | 107 | .PHONY: check-code-security 108 | check-code-security: 109 | bundle exec brakeman 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Killswitch is a clever control panel built by Mirego that allows mobile developers to apply
runtime version-specific behaviors to their iOS or Android application.

4 | 5 |

6 |
7 | 8 | 9 | 10 | 14 |
Screenshot of Killswitch showing the logged out homepage 11 | Screenshot of Killswitch showing the logged in homepage 12 | Screenshot of Killswitch showing a project page 13 |
15 |
16 | 17 | | Section | Description | 18 | | --------------------------------------- | ------------------------------------------------------ | 19 | | [🚧 Dependencies](#-dependencies) | Technical dependencies and how to install them | 20 | | [🏎 Kickstart](#-kickstart) | Details on how to kickstart development on the project | 21 | | [🏇 Usage](#-usage) | Details on how to use the application | 22 | | [🚑 Troubleshooting](#-troubleshooting) | Recurring problems and proven solutions | 23 | | [🚀 Deploy](#-deploy) | Deployment instructions | 24 | 25 | ## 🚧 Dependencies 26 | 27 | Every runtime dependencies are defined in the `.tool-versions` file. These external dependencies are also required: 28 | 29 | - PostgreSQL (`~> 10.0`) 30 | 31 | ## 🏎 Kickstart 32 | 33 | ### Environment variables 34 | 35 | All required environment variables are documented in [`.env.dev`](./.env.dev). 36 | 37 | When running `rails`, `rake` or `make` commands, it is important that these variables are present in the environment. There are several ways to achieve this. Using [`nv`](https://github.com/jcouture/nv) is recommended since it works out of the box with `.env.*` files. 38 | 39 | ### Initial setup 40 | 41 | 1. Create both `.env.dev.local` and `.env.test.local` from empty values in [`.env.dev`](./.env.dev) and [`.env.test`](./.env.test) 42 | 2. Install Ruby and NPM dependencies with `make dependencies` 43 | 3. Generate values for mandatory secrets in [`.env.dev`](./.env.dev) with `rake secret` 44 | 45 | Then, with variables from `.env.dev` and `.env.dev.local` present in the environment: 46 | 47 | 4. Create and migrate the database with `rake db:setup` 48 | 5. Start the Rails server with `rails s` 49 | 50 | ### Sample data 51 | 52 | You can use the `sample_data:create` Rake task to create an organization, an admin user and a few applications. 53 | 54 | ```shell-session 55 | $ KILLSWITCH_SAMPLE_DATA_EMAIL=foo@example.com KILLSWITCH_SAMPLE_DATA_PASSWORD=p@ssw0rd rake sample_data:create 56 | ``` 57 | 58 | ### `make` commands 59 | 60 | A `Makefile` is present at the root and expose common tasks. The list of these commands is available with `make help`. 61 | 62 | ### Tests 63 | 64 | Tests can be ran with `make test`. 65 | 66 | ### Linting 67 | 68 | Several linting and formatting tools can be ran to ensure coding style consistency: 69 | 70 | - `make lint` ensures code follows our guidelines and best practices 71 | - `make format` formats files using Prettier 72 | 73 | ### Continuous integration 74 | 75 | The `.github/workflows/ci.yaml` workflow ensures that the codebase is in good shape on each pull request and branch push. 76 | 77 | ## 🏇 Usage 78 | 79 | ### HTTP requests 80 | 81 | #### Request a behavior 82 | 83 | ``` 84 | GET /killswitch 85 | ``` 86 | 87 | ##### Parameters 88 | 89 | | Field | Type | Description | 90 | | --------- | ------ | -------------------------------------------------------------------------------------------- | 91 | | `key` | String | The API key (eg. `"f206934e29160b43924308251b88"`) | 92 | | `version` | String | The version of the application to test against (see _Conventions → Version numbers_ section) | 93 | 94 | ##### Headers 95 | 96 | | Header | Description | 97 | | ----------------- | ---------------------------------------------------------------------------------------------------------- | 98 | | `Accept-Language` | The application language (the API will return localized messages, if available (eg. `Accept-Language: fr`) | 99 | 100 | ##### Possible erroneous responses 101 | 102 | | Status | Code | Reason | 103 | | ------ | ----------- | ----------------------------------------------------- | 104 | | `400` | Bad Request | Parameters are malformed or missing | 105 | | `404` | Not Found | The API key is not for a valid project or application | 106 | 107 | ##### Possible successful responses 108 | 109 | | Status | Code | Message | 110 | | ------ | ------------ | --------------------------------------------------------------------------------------------------------------- | 111 | | `200` | OK | Everything is under control, yay | 112 | | `304` | Not Modified | If an `If-None-Match` header is passed and a cached response exist, a `304` will be returned with an empty body | 113 | 114 | ### Conventions 115 | 116 | #### Version numbers 117 | 118 | Version numbers supported by Killswitch must match the following regular expression: 119 | 120 | ``` 121 | /^\d+(\.\d+)?(\.\d+)?(\.\w+)?$/ 122 | ``` 123 | 124 | Here are some valid and invalid version number examples: 125 | 126 | | Version number | Valid | 127 | | -------------- | ------------------ | 128 | | `1` | :white_check_mark: | 129 | | `1.2` | :white_check_mark: | 130 | | `1.2.3` | :white_check_mark: | 131 | | `1.2.3.4` | :white_check_mark: | 132 | | `1.2.3.foo` | :white_check_mark: | 133 | | `foo` | :x: | 134 | | `1.` | :x: | 135 | | `1.2.` | :x: | 136 | | `1.2.3.4.5` | :x: | 137 | 138 | ### Behaviors JSON representations 139 | 140 | #### Root element 141 | 142 | | Key | Type | Description | 143 | | --------- | ------ | ------------------------------------------------------------------------- | 144 | | `action` | String | The action the application should enforce (`"ok"`, `"alert"` or `"kill"`) | 145 | | `message` | String | A message to display to the user | 146 | | `buttons` | Array | An array of buttons to show to the user | 147 | 148 | ##### Buttons 149 | 150 | | Key | Type | Description | 151 | | ------- | ------ | ---------------------------------------------------------------------------------------------- | 152 | | `type` | String | The type of button (`"cancel`, `"url"` or `"reload"`), defaults to `"cancel"` if not specified | 153 | | `label` | String | The button label | 154 | | `url` | String | The button URL, if the type is `"url"` | 155 | 156 | #### Examples 157 | 158 | ##### OK 159 | 160 | ```json 161 | { 162 | "action": "ok", 163 | "buttons": [] 164 | } 165 | ``` 166 | 167 | ##### Alert 168 | 169 | ```json 170 | { 171 | "action": "alert", 172 | "message": "SUP?", 173 | "buttons": [ 174 | { 175 | "type": "cancel", 176 | "label": "Nothing" 177 | } 178 | ] 179 | } 180 | ``` 181 | 182 | ##### Kill 183 | 184 | ```json 185 | { 186 | "action": "kill", 187 | "message": "The app will be killed now. Please upgrade to the latest version.", 188 | "buttons": [ 189 | { 190 | "type": "url", 191 | "label": "Upgrade", 192 | "url": "itms://foo" 193 | }, 194 | { 195 | "type": "cancel", 196 | "label": "I don’t want to." 197 | } 198 | ] 199 | } 200 | ``` 201 | 202 | ## 🚑 Troubleshooting 203 | 204 | ### System readiness 205 | 206 | The project exposes a `GET /ping` route that sends an HTTP `200 OK` response as soon as the server is ready to accept requests. The response also contains the project version for debugging purpose. 207 | 208 | ### System health 209 | 210 | The project exposes a `GET /health` route that contains checks to make sure the application and its external dependencies are healthy. 211 | 212 | | Name | Description | 213 | | ---------- | ------------------------------------------ | 214 | | `noop` | This check is always healthy | 215 | | `database` | Check if the database connection is active | 216 | 217 | ## 🚀 Deploy 218 | 219 | ### Container 220 | 221 | A Docker image can be created with `make build` and pushed to a registry with `make push`. 222 | 223 | [Official Docker images](https://github.com/mirego/killswitch/pkgs/container/killswitch) can be pulled from GitHub Container registry. For example, to get the latest `main` image: 224 | 225 | ```shell-session 226 | $ docker pull ghcr.io/mirego/killswitch:latest 227 | ``` 228 | 229 | ## License 230 | 231 | Killswitch is © 2013-2022 [Mirego](https://www.mirego.com) and may be freely distributed under the [New BSD license](http://opensource.org/licenses/BSD-3-Clause). See the [`LICENSE.md`](https://github.com/mirego/killswitch/blob/main/LICENSE.md) file. 232 | 233 | The shield logo is based on [this lovely icon by Kimmi Studio](https://thenounproject.com/icon/shield-1055246/), from The Noun Project. Used under a [Creative Commons BY 3.0](http://creativecommons.org/licenses/by/3.0/) license. 234 | 235 | ## About Mirego 236 | 237 | [Mirego](https://www.mirego.com) is a team of passionate people who believe that work is a place where you can innovate and have fun. We’re a team of [talented people](https://life.mirego.com) who imagine and build beautiful Web and mobile applications. We come together to share ideas and [change the world](http://www.mirego.org). 238 | 239 | We also [love open-source software](https://open.mirego.com) and we try to give back to the community as much as we can. 240 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path('../config/application', __FILE__) 7 | 8 | Killswitch::Application.load_tasks 9 | 10 | # Load tasks in app/tasks 11 | Rails.root.glob('app/tasks/**/*.rake').each { |f| load f } 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.2.x | :white_check_mark: | 8 | | 1.1.x | :x: | 9 | | 1.0.x | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please disclose security vulnerabilities privately at rprevost@mirego.com. 14 | -------------------------------------------------------------------------------- /app/assets/images/img-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/killswitch/d747029c477e80aecb94eea1fb60999ad753b83a/app/assets/images/img-favicon.png -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // = require_tree ./components 2 | // = require ./main 3 | 4 | require(['main']); 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/components/app.es6: -------------------------------------------------------------------------------- 1 | class App { 2 | constructor() { 3 | this.registeredComponents = []; 4 | this.instanciatedComponents = []; 5 | this.$self = $(this); 6 | 7 | const dispatchUnloadEvent = function () { 8 | const event = document.createEvent('Events'); 9 | event.initEvent('turbolinks:unload', true, false); 10 | document.dispatchEvent(event); 11 | }; 12 | 13 | document.addEventListener('beforeunload', dispatchUnloadEvent); 14 | document.addEventListener('turbolinks:before-render', dispatchUnloadEvent); 15 | 16 | window.$window = $(window); 17 | window.$document = $(document); 18 | window.$document.on('turbolinks:load', () => this.pageDidLoad()); 19 | window.$document.on('turbolinks:unload', () => this.pageWillUnload()); 20 | } 21 | 22 | initialize() { 23 | this.instanciateComponents(); 24 | return this; 25 | } 26 | 27 | pageDidLoad() { 28 | this.instanciateComponents(); 29 | } 30 | 31 | pageWillUnload() { 32 | this.destroyComponents(); 33 | } 34 | 35 | registerComponent(componentClass, componentSelector) { 36 | this.registeredComponents.push({ 37 | class: componentClass, 38 | selector: componentSelector, 39 | arguments: 40 | arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null 41 | }); 42 | 43 | return this; 44 | } 45 | 46 | instanciateComponents() { 47 | this.registeredComponents.forEach((component) => { 48 | const components = $(component.selector) 49 | .toArray() 50 | .map((element) => { 51 | const constructor = component.class; 52 | if (component.arguments) { 53 | component.arguments.unshift(element); 54 | 55 | const F = function () { 56 | return constructor.apply(this, component.arguments); 57 | }; 58 | F.prototype = constructor.prototype; 59 | return new F(); 60 | } else { 61 | return new constructor(element); 62 | } 63 | }); 64 | 65 | this.instanciatedComponents = 66 | this.instanciatedComponents.concat(components); 67 | }); 68 | 69 | return this; 70 | } 71 | 72 | destroyComponents() { 73 | this.instanciatedComponents.forEach((component) => component.destroy()); 74 | this.instanciatedComponents.splice(0, this.instanciatedComponents.length); 75 | return this; 76 | } 77 | } 78 | 79 | export default App; 80 | -------------------------------------------------------------------------------- /app/assets/javascripts/components/behavior-populate-dropdown.es6: -------------------------------------------------------------------------------- 1 | import Component from 'components/component'; 2 | 3 | class BehaviorPopulateDropdown extends Component { 4 | constructor(element) { 5 | super(element); 6 | this.linkElements = this.$element.find('a'); 7 | this.textField = this.$element 8 | .closest('.btn-group') 9 | .siblings('textarea, input:text'); 10 | this.bindEvents(); 11 | } 12 | 13 | bindEvents() { 14 | this.linkElements.on('click.BehaviorPopulateDropdown', (event) => 15 | this.handleClick(event) 16 | ); 17 | } 18 | 19 | unbindEvents() { 20 | this.linkElements.off('click.BehaviorPopulateDropdown'); 21 | } 22 | 23 | handleClick(event) { 24 | const $target = $(event.currentTarget); 25 | const templateAsObject = JSON.parse($target.attr('data-template')); 26 | const templateAsPrettyString = JSON.stringify( 27 | templateAsObject, 28 | undefined, 29 | 2 30 | ); 31 | 32 | this.textField.val(templateAsPrettyString); 33 | } 34 | } 35 | 36 | export default BehaviorPopulateDropdown; 37 | -------------------------------------------------------------------------------- /app/assets/javascripts/components/component.es6: -------------------------------------------------------------------------------- 1 | class Component { 2 | constructor(element) { 3 | this.$element = $(element); 4 | } 5 | 6 | destroy() { 7 | this.unbindEvents(); 8 | } 9 | 10 | bindEvents() {} 11 | unbindEvents() {} 12 | } 13 | 14 | export default Component; 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/components/orderable-behavior.es6: -------------------------------------------------------------------------------- 1 | import Component from 'components/component'; 2 | 3 | class OrderableBehavior extends Component { 4 | constructor(element) { 5 | super(element); 6 | 7 | this.toggleElement = this.$element.find('a.toggle'); 8 | sortable(this.$element); 9 | this.bindEvents(); 10 | } 11 | 12 | bindEvents() { 13 | this.$element.on('sortupdate.OrderableBehavior', () => this.handleUpdate()); 14 | this.toggleElement.on('click.OrderableBehavior', (event) => 15 | this.handleDataToggle(event) 16 | ); 17 | } 18 | 19 | unbindEvents() { 20 | this.$element.off('sortupdate.OrderableBehavior'); 21 | this.toggleElement.off('click.OrderableBehavior'); 22 | } 23 | 24 | handleUpdate() { 25 | const mapId = (_, element) => { 26 | return $(element).data('id'); 27 | }; 28 | const behaviorIds = this.$element.children().map(mapId).toArray(); 29 | const url = this.$element.data('order-url'); 30 | 31 | const data = {behaviors: behaviorIds}; 32 | $.ajax({type: 'PUT', url, data}); 33 | } 34 | 35 | handleDataToggle(event) { 36 | $(event.target).closest('li').find('.data-row').toggle(); 37 | } 38 | } 39 | 40 | export default OrderableBehavior; 41 | -------------------------------------------------------------------------------- /app/assets/javascripts/components/waiting-bar.es6: -------------------------------------------------------------------------------- 1 | import Component from 'components/component'; 2 | 3 | class WaitingBar extends Component { 4 | constructor(element) { 5 | super(element); 6 | this.bindEvents(); 7 | } 8 | 9 | bindEvents() { 10 | window.$document.on('page:fetch.WaitingBar', () => this.handlePageFetch()); 11 | } 12 | 13 | unbindEvents() { 14 | window.$document.off('page:fetch.WaitingBar'); 15 | } 16 | 17 | handlePageFetch() { 18 | this.$element.show(); 19 | } 20 | } 21 | 22 | export default WaitingBar; 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/main.es6: -------------------------------------------------------------------------------- 1 | import App from 'components/app'; 2 | import OrderableBehavior from 'components/orderable-behavior'; 3 | import BehaviorPopulateDropdown from 'components/behavior-populate-dropdown'; 4 | import WaitingBar from 'components/waiting-bar'; 5 | 6 | $(() => { 7 | // Initialize application components 8 | window.App = new App(); 9 | window.App.registerComponent(OrderableBehavior, '.behaviors-list') 10 | .registerComponent(BehaviorPopulateDropdown, '.behavior-populate') 11 | .registerComponent(WaitingBar, '.waiting-bar') 12 | .initialize(); 13 | }); 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/vendor.js: -------------------------------------------------------------------------------- 1 | // = require jquery/dist/jquery.js 2 | // = require jquery-ujs/src/rails.js 3 | // = require bootstrap/dist/js/bootstrap.js 4 | // = require html5sortable/dist/html5sortable.js 5 | // = require turbolinks 6 | // = require almond/almond.js 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | min-height: 100vh; 4 | background: #fff; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 6 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; 7 | font-size: 14px; 8 | line-height: 1.4; 9 | color: #333; 10 | -webkit-font-smoothing: antialiased; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | .h1, 20 | .h2, 21 | .h3, 22 | .h4, 23 | .h5, 24 | .h6 { 25 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 26 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; 27 | } 28 | 29 | main { 30 | width: 100%; 31 | padding: 75px 15px 25px; 32 | } 33 | 34 | h1 { 35 | margin: 0 0 20px; 36 | } 37 | 38 | h4 { 39 | margin-top: 30px; 40 | } 41 | 42 | p:last-child { 43 | margin-bottom: 0; 44 | } 45 | 46 | .fixed-width-font { 47 | padding: 10px; 48 | font-family: monospace; 49 | font-size: 12px; 50 | } 51 | 52 | pre { 53 | border: 0; 54 | margin-bottom: 20px; 55 | font-size: 12px; 56 | 57 | &.inverse { 58 | background: #222; 59 | color: rgba(#fff, 0.75); 60 | } 61 | } 62 | 63 | .form-actions { 64 | margin: 20px 0 0; 65 | } 66 | 67 | .dropdown-menu { 68 | overflow-y: auto; 69 | max-height: 400px; 70 | } 71 | 72 | .navbar-brand-wrap { 73 | display: flex; 74 | justify-content: center; 75 | } 76 | 77 | .navbar-brand-image { 78 | width: 20px; 79 | height: 20px; 80 | margin: 0 10px 0 0; 81 | } 82 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | //= require bootstrap/dist/css/bootstrap.css 2 | //= require bootstrap-theme 3 | //= require font-awesome 4 | //= require_self 5 | 6 | @import 'base'; 7 | @import 'components/*'; 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | a, 2 | .btn { 3 | color: #3d6dc1; 4 | } 5 | 6 | a:hover, 7 | .btn:hover { 8 | color: #2b4d9f; 9 | } 10 | 11 | .navbar-fixed-top .navbar-collapse { 12 | padding-right: 15px; 13 | padding-left: 15px; 14 | } 15 | 16 | .btn-default, 17 | .btn-primary, 18 | .btn-success, 19 | .btn-info, 20 | .btn-warning, 21 | .btn-danger, 22 | .btn-default:hover, 23 | .btn-primary:hover, 24 | .btn-success:hover, 25 | .btn-info:hover, 26 | .btn-warning:hover, 27 | .btn-danger:hover { 28 | font-weight: bold; 29 | color: #fff; 30 | } 31 | 32 | .btn-default, 33 | .btn-default:hover { 34 | background: #ddd; 35 | color: #333; 36 | } 37 | 38 | .btn-primary, 39 | .btn-primary:hover { 40 | background: #3d6dc1; 41 | } 42 | 43 | .navbar-inverse { 44 | background: #333; 45 | } 46 | 47 | .navbar-inverse .navbar-nav > .active > a { 48 | background: #222; 49 | } 50 | 51 | .navbar-static-top, 52 | .navbar-fixed-top, 53 | .navbar-fixed-bottom { 54 | border-radius: 0; 55 | } 56 | 57 | .alert { 58 | border: 0; 59 | text-shadow: none; 60 | } 61 | 62 | .alert-success { 63 | background: #dff0d8; 64 | } 65 | 66 | .alert-info { 67 | background: #d9edf7; 68 | } 69 | 70 | .alert-warning { 71 | background: #fcf8e3; 72 | } 73 | 74 | .alert-danger { 75 | background: #f2dede; 76 | } 77 | 78 | .table-bordered { 79 | border-radius: 2px; 80 | } 81 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_applications_list.scss: -------------------------------------------------------------------------------- 1 | .applications-list { 2 | padding: 0; 3 | 4 | > li { 5 | padding: 15px; 6 | border: 2px solid #ddd; 7 | border-radius: 5px; 8 | margin: 0 0 15px; 9 | list-style: none; 10 | 11 | .application-name { 12 | margin: 0 0 15px; 13 | font-size: 24px; 14 | font-weight: bold; 15 | } 16 | 17 | ul { 18 | padding: 0; 19 | 20 | li { 21 | display: inline-block; 22 | margin: 0 10px 0 0; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_behaviors_list.scss: -------------------------------------------------------------------------------- 1 | .behaviors-list { 2 | padding: 0; 3 | margin: 0 0 20px; 4 | 5 | li { 6 | list-style: none; 7 | 8 | &.sortable-placeholder { 9 | width: auto; 10 | height: 43px; 11 | border-top: 1px solid #ddd; 12 | margin: 0 -15px; 13 | background: #fffde6; 14 | } 15 | 16 | &:last-child { 17 | .row { 18 | border-bottom: 1px solid #ddd; 19 | } 20 | } 21 | 22 | .row { 23 | border-top: 1px solid #ddd; 24 | cursor: move; 25 | 26 | > div { 27 | padding-top: 10px; 28 | padding-bottom: 10px; 29 | border-right: 1px solid #ddd; 30 | } 31 | } 32 | 33 | .data-row { 34 | display: none; 35 | padding: 0 0 30px; 36 | 37 | pre { 38 | border: 0; 39 | border-bottom: 1px solid #ddd; 40 | border-radius: 0; 41 | margin: 0; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_error_messages.scss: -------------------------------------------------------------------------------- 1 | .error-messages { 2 | ul { 3 | padding: 0; 4 | margin-left: 20px; 5 | } 6 | 7 | .intro { 8 | margin: 0 0 8px; 9 | font-weight: bold; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_homepage.scss: -------------------------------------------------------------------------------- 1 | .homepage { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 100%; 6 | padding: 40px; 7 | background: radial-gradient(circle, #4289cc 0%, #124575 87%); 8 | color: #fff; 9 | } 10 | 11 | .homepage-content { 12 | width: 400px; 13 | text-align: center; 14 | } 15 | 16 | .homepage-logo { 17 | display: block; 18 | margin: 0 0 40px; 19 | outline: none; 20 | 21 | &:focus, 22 | &:hover { 23 | opacity: 0.9; 24 | } 25 | } 26 | 27 | .homepage-logo-image { 28 | width: 300px; 29 | height: 300px; 30 | } 31 | 32 | .homepage-intro { 33 | margin: 0 0 35px; 34 | line-height: 1.6; 35 | 36 | a { 37 | font-weight: bold; 38 | color: #fff; 39 | 40 | &:focus, 41 | &:hover { 42 | color: rgba(#fff, 0.9); 43 | } 44 | } 45 | } 46 | 47 | .homepage-version { 48 | opacity: 0.5; 49 | margin-top: 30px; 50 | font-size: 12px; 51 | } 52 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_waiting_bar.scss: -------------------------------------------------------------------------------- 1 | .waiting-bar { 2 | display: none; 3 | position: absolute; 4 | z-index: 100000; 5 | bottom: -4px; 6 | left: 0; 7 | width: 100%; 8 | height: 3px; 9 | background-color: #3d6dc1; 10 | background-image: linear-gradient( 11 | -45deg, 12 | rgba(#fff, 0.15) 25%, 13 | transparent 25%, 14 | transparent 50%, 15 | rgba(#fff, 0.15) 50%, 16 | rgba(#fff, 0.15) 75%, 17 | transparent 75%, 18 | transparent 19 | ); 20 | background-repeat: repeat-x; 21 | background-size: 30px 30px; 22 | animation: progress 0.5s linear infinite; 23 | } 24 | 25 | @keyframes progress { 26 | to { 27 | background-position: 30px 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/controllers/api/application_controller.rb: -------------------------------------------------------------------------------- 1 | class API::ApplicationController < ApplicationController 2 | protect_from_forgery with: :null_session 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/api/behaviors_controller.rb: -------------------------------------------------------------------------------- 1 | class API::BehaviorsController < API::ApplicationController 2 | # Constants 3 | VARY_HEADER = 'Accept-Language'.freeze 4 | 5 | # GET /killswitch 6 | def show 7 | dispatcher = BehaviorDispatcher.new 8 | dispatcher.dispatch!(request) 9 | 10 | headers['Vary'] = VARY_HEADER 11 | render json: dispatcher.matching_behavior 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/api/errors_controller.rb: -------------------------------------------------------------------------------- 1 | class API::ErrorsController < API::ApplicationController 2 | include Gaffe::Errors 3 | 4 | def show 5 | render json: { error: @rescue_response }, status: @status_code 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/killswitch/d747029c477e80aecb94eea1fb60999ad753b83a/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/web/application_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::ApplicationController < ApplicationController 2 | # CSRF Protection 3 | protect_from_forgery with: :exception 4 | 5 | # Users must be authenticated everywhere 6 | before_action :authenticate_user! 7 | before_action :set_paper_trail_whodunnit 8 | 9 | # Expose some methods to views 10 | helper_method :current_organization 11 | 12 | protected 13 | 14 | def current_organization 15 | return nil if params[:organization_id].blank? 16 | 17 | @current_organization ||= begin 18 | organization = Organization.friendly.find(params[:organization_id]) 19 | organization if can?(:access, organization) 20 | end 21 | rescue ActiveRecord::RecordNotFound 22 | nil 23 | end 24 | 25 | def after_sign_in_path_for(resource) 26 | safe_redirect_path(stored_location_for(resource)) 27 | end 28 | 29 | def after_sign_out_path_for(*) 30 | safe_redirect_path(request.referer) 31 | end 32 | 33 | # Returns the path only if it responds to GET requests. Otherwise returns root. 34 | def safe_redirect_path(path) 35 | return root_path if path.blank? 36 | 37 | Rails.application.routes.recognize_path(path, method: :get) 38 | rescue ActionController::RoutingError 39 | root_path 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/web/applications_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::ApplicationsController < Web::ApplicationController 2 | before_action :fetch_organization 3 | before_action :fetch_application, only: [:show, :update, :edit, :destroy] 4 | 5 | # GET / 6 | # GET /applications 7 | def index 8 | @applications = @organization.applications.includes(:projects) 9 | end 10 | 11 | # GET /applications/:id 12 | def show; end 13 | 14 | # GET /applications/new 15 | def new 16 | @application = @organization.applications.build 17 | end 18 | 19 | # GET /applications/:id/edit 20 | def edit; end 21 | 22 | # POST /applications 23 | def create 24 | @application = @organization.applications.build(application_params) 25 | 26 | if @application.save 27 | redirect_to web_organization_application_path(id: @application), notice: t('.notice') 28 | else 29 | render :new 30 | end 31 | end 32 | 33 | # PUT /applications 34 | # PATCH /applications/:id 35 | def update 36 | if @application.update(application_params) 37 | redirect_to web_organization_application_path, notice: t('.notice') 38 | else 39 | render :edit 40 | end 41 | end 42 | 43 | # DELETE /applications/:id 44 | def destroy 45 | if @application.destroy 46 | redirect_to web_organization_applications_path, notice: t('.notice') 47 | else 48 | redirect_to web_organization_applications_path, alert: t('.alert') 49 | end 50 | end 51 | 52 | protected 53 | 54 | def fetch_organization 55 | @organization = Organization.friendly.find(params[:organization_id]) 56 | 57 | authorize! :access, @organization 58 | end 59 | 60 | def fetch_application 61 | @application = @organization.applications.friendly.find(params[:id]) 62 | end 63 | 64 | private 65 | 66 | def application_params 67 | permitted_parameters = [:name] 68 | params.require(:application).permit(*permitted_parameters) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/controllers/web/behaviors_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::BehaviorsController < Web::ApplicationController 2 | before_action :fetch_organization 3 | before_action :fetch_application 4 | before_action :fetch_project 5 | before_action :fetch_behavior, only: [:update, :edit, :destroy] 6 | 7 | # GET /applications/:application_id/projects/:project_id/behaviors/new 8 | def new 9 | @behavior = @project.behaviors.build 10 | end 11 | 12 | # GET /applications/:application_id/projects/:project_id/behaviors/:id 13 | def edit; end 14 | 15 | # POST /applications/:application_id/projects/:project_id/behaviors 16 | def create 17 | @behavior = @project.behaviors.build(behavior_params) 18 | 19 | if @behavior.save 20 | redirect_to web_organization_application_project_path(application_id: @application, id: @project), notice: t('.notice') 21 | else 22 | render :new 23 | end 24 | end 25 | 26 | # PUT /applications/:application_id/projects/:project_id/behaviors/:id 27 | # PATCH /applications/:application_id/projects/:project_id/behaviors/:id 28 | def update 29 | if @behavior.update(behavior_params) 30 | redirect_to web_organization_application_project_path(application_id: @application, id: @project), notice: t('.notice') 31 | else 32 | render :edit 33 | end 34 | end 35 | 36 | # DELETE /applications/:application_id/projects/:project_id/behaviors/:id 37 | def destroy 38 | if @behavior.destroy 39 | redirect_to web_organization_application_project_path(application_id: @application, id: @project), notice: t('.notice') 40 | else 41 | redirect_to web_organization_application_project_path(application_id: @application, id: @project), alert: t('.alert') 42 | end 43 | end 44 | 45 | # PUT /applications/:application_id/projects/:project_id/behaviors/order 46 | def order 47 | sorter = BehaviorSorter.new(@project) 48 | sorter.reorder!(params[:behaviors]) 49 | 50 | head :ok 51 | end 52 | 53 | protected 54 | 55 | def fetch_organization 56 | @organization = Organization.friendly.find(params[:organization_id]) 57 | 58 | authorize! :access, @organization 59 | end 60 | 61 | def fetch_application 62 | @application = @organization.applications.friendly.find(params[:application_id]) 63 | end 64 | 65 | def fetch_project 66 | @project = @application.projects.friendly.find(params[:project_id]) 67 | end 68 | 69 | def fetch_behavior 70 | @behavior = @project.behaviors.find(params[:id]) 71 | end 72 | 73 | private 74 | 75 | def behavior_params 76 | permitted_parameters = [:version_number, :version_operator, :time, :time_operator, :language, :data] 77 | params.require(:behavior).permit(*permitted_parameters) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/controllers/web/errors_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::ErrorsController < Web::ApplicationController 2 | include Gaffe::Errors 3 | 4 | # Layout 5 | layout 'application' 6 | 7 | # Allow all users to view error pages 8 | skip_before_action :authenticate_user! 9 | 10 | def show 11 | render "web/errors/#{@rescue_response}", status: @status_code 12 | rescue ActionView::MissingTemplate 13 | render 'web/errors/internal_server_error', status: @status_code 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/web/home_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::HomeController < Web::ApplicationController 2 | skip_before_action :authenticate_user! 3 | 4 | def show 5 | return if current_user.blank? 6 | 7 | organization = current_user.organizations.first 8 | return if organization.blank? 9 | 10 | redirect_to web_organization_applications_path(organization_id: organization) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/web/memberships_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::MembershipsController < Web::ApplicationController 2 | before_action :fetch_organization 3 | before_action :fetch_membership, only: [:update, :edit, :destroy] 4 | 5 | # GET /memberships 6 | def index 7 | @memberships = @organization.memberships.oldest 8 | end 9 | 10 | # GET /memberships/new 11 | def new 12 | @membership = @organization.admins.new(user: User.new) 13 | end 14 | 15 | # GET /memberships/:id/edit 16 | def edit; end 17 | 18 | # POST /memberships 19 | def create 20 | @membership = @organization.memberships.new(membership_params(context: :create)) 21 | 22 | if @membership.save 23 | redirect_to web_organization_memberships_path, notice: t('.notice') 24 | else 25 | render :new 26 | end 27 | end 28 | 29 | # PUT /memberships/:id 30 | # PATCH /memberships/:id 31 | def update 32 | if @membership.update(membership_params(context: :update)) 33 | redirect_to web_organization_memberships_path, notice: t('.notice') 34 | else 35 | render :edit 36 | end 37 | end 38 | 39 | # DELETE /memberships/:id 40 | def destroy 41 | if @membership.destroy 42 | redirect_to web_organization_memberships_path, notice: t('.notice') 43 | else 44 | redirect_to web_organization_memberships_path, alert: t('.alert') 45 | end 46 | end 47 | 48 | protected 49 | 50 | def fetch_organization 51 | @organization = Organization.friendly.find(params[:organization_id]) 52 | 53 | authorize! :access, @organization 54 | end 55 | 56 | def fetch_membership 57 | @membership = @organization.memberships.find(params[:id]) 58 | end 59 | 60 | private 61 | 62 | def create_membership_params 63 | [{ user_attributes: [:name, :email] }, :membership_type] 64 | end 65 | 66 | def update_membership_params 67 | [:membership_type] 68 | end 69 | 70 | def membership_params(context: :create) 71 | permitted_parameters = send(:"#{context}_membership_params") 72 | params.require(:membership).permit(*permitted_parameters).delete_if { |_, value| value.empty? } 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /app/controllers/web/organizations_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::OrganizationsController < Web::ApplicationController 2 | before_action :fetch_organizations, only: [:index] 3 | before_action :fetch_organization, only: [:edit, :update, :destroy] 4 | 5 | # GET /organizations 6 | def index 7 | authorize! :manage, Organization 8 | end 9 | 10 | # GET /organizations/new 11 | def new 12 | @organization = Organization.new 13 | end 14 | 15 | # GET /organizations/:id/edit 16 | def edit; end 17 | 18 | # POST /organizations 19 | def create 20 | @organization = Organization.new(organization_params) 21 | 22 | if @organization.save 23 | redirect_to web_organizations_path, notice: t('.notice') 24 | else 25 | render :new 26 | end 27 | end 28 | 29 | # PUT /organizations/:id 30 | # PATCH /organizations/:id 31 | def update 32 | if @organization.update(organization_params) 33 | redirect_to web_organizations_path, notice: t('.notice') 34 | else 35 | render :edit 36 | end 37 | end 38 | 39 | # DELETE /organizations/:id 40 | def destroy 41 | if @organization.destroy 42 | redirect_to web_organizations_path, notice: t('.notice') 43 | else 44 | redirect_to web_organizations_path, alert: t('.alert') 45 | end 46 | end 47 | 48 | protected 49 | 50 | def fetch_organizations 51 | @organizations = Organization.ascendingly 52 | end 53 | 54 | def fetch_organization 55 | @organization = Organization.friendly.find(params[:id]) 56 | end 57 | 58 | def organization_params 59 | permitted_parameters = [:name] 60 | params.require(:organization).permit(*permitted_parameters) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/controllers/web/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::PasswordsController < Devise::PasswordsController 2 | protected 3 | 4 | # Where to redirect after users reset their password 5 | def after_resetting_password_path_for(*) 6 | root_path 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/web/projects_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::ProjectsController < Web::ApplicationController 2 | before_action :fetch_organization 3 | before_action :fetch_application 4 | before_action :fetch_project, only: [:show, :update, :edit, :destroy] 5 | 6 | # GET /applications/:application_id/projects/:id 7 | def show; end 8 | 9 | # GET /applications/:application_id/projects/new 10 | def new 11 | @project = @application.projects.build 12 | end 13 | 14 | # GET /applications/:application_id/projects/:id/edit 15 | def edit; end 16 | 17 | # POST /applications/:application_id/projects 18 | def create 19 | @project = @application.projects.build(project_params) 20 | 21 | if @project.save 22 | redirect_to web_organization_application_path(id: @application), notice: t('.notice') 23 | else 24 | render :new 25 | end 26 | end 27 | 28 | # PUT /applications/:application_id/projects/:id 29 | # PATH /applications/:application_id/projects/:id 30 | def update 31 | if @project.update(project_params) 32 | redirect_to web_organization_application_project_path, notice: t('.notice') 33 | else 34 | render :edit 35 | end 36 | end 37 | 38 | # GET /applications/:application_id/projects/:id 39 | def destroy 40 | if @project.destroy 41 | redirect_to web_organization_application_path(id: @application), notice: t('.notice') 42 | else 43 | redirect_to web_organization_application_path(id: @application), alert: t('.alert') 44 | end 45 | end 46 | 47 | protected 48 | 49 | def fetch_organization 50 | @organization = Organization.friendly.find(params[:organization_id]) 51 | 52 | authorize! :access, @organization 53 | end 54 | 55 | def fetch_application 56 | @application = @organization.applications.friendly.find(params[:application_id]) 57 | end 58 | 59 | def fetch_project 60 | @project = @application.projects.friendly.find(params[:id]) 61 | end 62 | 63 | private 64 | 65 | def project_params 66 | permitted_parameters = [:name] 67 | params.require(:project).permit(*permitted_parameters) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/controllers/web/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::SessionsController < Devise::SessionsController 2 | before_action :configure_permitted_parameters 3 | 4 | protected 5 | 6 | # White-list parameters for Devise 7 | def configure_permitted_parameters 8 | devise_parameter_sanitizer.permit(:sign_in, keys: [:email, :password, :remember_me]) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/web/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::UsersController < Web::ApplicationController 2 | before_action :fetch_user, only: [:update, :edit] 3 | 4 | # GET /users/:id/edit 5 | def edit; end 6 | 7 | # PUT /users/:id 8 | # PATCH /users/:id 9 | def update 10 | if @user.update(user_params) 11 | redirect_to root_path, notice: t('.notice') 12 | else 13 | render :edit 14 | end 15 | end 16 | 17 | protected 18 | 19 | def fetch_user 20 | @user = User.friendly.find(params[:id]) 21 | end 22 | 23 | private 24 | 25 | def user_params 26 | permitted_parameters = [:name, :email, :password] 27 | params.require(:user).permit(*permitted_parameters).delete_if { |_, value| value.empty? } 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/helpers/devise_helper.rb: -------------------------------------------------------------------------------- 1 | module DeviseHelper 2 | # Overwrite Devise error messages helper 3 | def devise_error_messages! 4 | return unless resource.errors.any? 5 | 6 | render partial: 'shared/error_messages', locals: { record: resource } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/error_helper.rb: -------------------------------------------------------------------------------- 1 | module ErrorHelper 2 | def render_error_messages(record) 3 | return unless record.errors.any? 4 | 5 | render partial: 'shared/error_messages', locals: { record: } 6 | end 7 | 8 | # Return an exception's bracktrace formatted into a nice 9 | # plain list. It also wraps lines from the application 10 | # directory in elements. 11 | def pretty_backtrace(exception) 12 | exception.backtrace.map do |line| 13 | if line =~ %r{#{Rails.root}} 14 | content_tag(:span, line, class: 'app-line') 15 | else 16 | line 17 | end 18 | end.join("\n") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/helpers/html_helper.rb: -------------------------------------------------------------------------------- 1 | module HtmlHelper 2 | def logo(options = {}) 3 | render partial: 'shared/logo', locals: { options: } 4 | end 5 | 6 | def icon(name, label = nil) 7 | content_tag(:i, '', class: "fa fa-#{name.to_s.tr('_', '-')}") + label.presence.try(:prepend, '  ') 8 | end 9 | 10 | def nav_active_item?(item) 11 | ' class="active"'.html_safe if item == content_for(:nav_active_item).try(:to_sym) 12 | end 13 | 14 | def page_title(base: '') 15 | parts = [] 16 | 17 | parts << content_for(:page_title) if content_for?(:page_title) 18 | parts << "(#{present(current_organization).name})" if current_organization.present? 19 | 20 | if parts.any? 21 | "#{parts.join(' ')} — #{base}" 22 | else 23 | base 24 | end 25 | end 26 | 27 | def bool_icon(value) 28 | value ? icon(:check) : nil 29 | end 30 | 31 | def version 32 | "v#{Killswitch::Application::VERSION}" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/helpers/mailer_helper.rb: -------------------------------------------------------------------------------- 1 | module MailerHelper 2 | # Return the value of the `style` attribute for an element, 3 | # based on default styles. 4 | def inline_styles(to_merge = {}) 5 | defaults = { 6 | # rubocop:disable Style/HashSyntax 7 | :'font-family' => 'Helvetica, Arial, sans-serif', 8 | :'font-weight' => 300, 9 | :'font-size' => '14px', 10 | :'line-height' => '1.5', 11 | :color => '#5c5c5c' 12 | # rubocop:enable Style/HashSyntax 13 | } 14 | 15 | defaults.merge(to_merge).reduce('') do |memo, (property, value)| 16 | memo << "#{property}: #{value}; " 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | # Layout 3 | layout 'mailer' 4 | 5 | # Defaults 6 | default from: Rails.application.secrets.mailer_from 7 | 8 | # Helpers 9 | helper MailerHelper 10 | end 11 | -------------------------------------------------------------------------------- /app/mailers/devise_mailer.rb: -------------------------------------------------------------------------------- 1 | class DeviseMailer < Devise::Mailer 2 | # Layout 3 | layout 'mailer' 4 | 5 | # Defaults 6 | default from: Rails.application.secrets.mailer_from 7 | 8 | # Helpers 9 | helper MailerHelper 10 | end 11 | -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | class UserMailer < ApplicationMailer 2 | def welcome_email(user_id, token) 3 | @user = User.find(user_id) 4 | @token = token 5 | mail to: @user.email, subject: t('.subject') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/killswitch/d747029c477e80aecb94eea1fb60999ad753b83a/app/models/.keep -------------------------------------------------------------------------------- /app/models/application.rb: -------------------------------------------------------------------------------- 1 | class Application < ApplicationRecord 2 | # Associations 3 | has_many :projects, -> { ascendingly }, dependent: :destroy, inverse_of: :application 4 | belongs_to :organization, counter_cache: true 5 | 6 | # Validations 7 | validates :name, presence: true, uniqueness: { scope: [:deleted_at] } 8 | 9 | # FriendlyId 10 | extend FriendlyId 11 | friendly_id :name, use: :slugged 12 | 13 | # PaperTrail 14 | has_paper_trail 15 | 16 | # Scopes 17 | scope(:ascendingly, -> { order(name: :asc) }) 18 | end 19 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/behavior.rb: -------------------------------------------------------------------------------- 1 | class Behavior < ApplicationRecord 2 | # Constants 3 | VERSION_OPERATORS = { 'lt' => :<, 'lte' => :<=, 'eq' => :==, 'gte' => :>=, 'gt' => :> }.freeze 4 | TIME_OPERATORS = { 'lt' => :<, 'gt' => :> }.freeze 5 | LANGUAGES = %w(fr en de es it pt).freeze 6 | DATA_JSON_SCHEMA = Rails.root.join('config/schemas/behavior_data.jsonschema') 7 | 8 | # Validations 9 | validates :version_number, presence: true, version: true 10 | validates :version_operator, presence: true, inclusion: { in: VERSION_OPERATORS.keys } 11 | validates :time, presence: true, if: :time_operator? 12 | validates :time_operator, presence: true, inclusion: { in: TIME_OPERATORS.keys }, if: :time? 13 | validates :language, inclusion: { in: LANGUAGES }, allow_nil: true 14 | validates :data, presence: true, json: { schema: DATA_JSON_SCHEMA, message: ->(error) { error } } 15 | 16 | # Associations 17 | belongs_to :project 18 | has_one :application, through: :project 19 | 20 | # RankModel 21 | include RankedModel 22 | ranks :behavior_order, with_same: :project_id 23 | 24 | # PaperTrail 25 | has_paper_trail 26 | 27 | # Scopes 28 | scope(:ascendingly, -> { rank(:behavior_order) }) 29 | 30 | # Make sure we set `language` to nil if we receive a blank value 31 | def language=(language) 32 | language.blank? ? super(nil) : super 33 | end 34 | 35 | # Return the "versionified" version 36 | def parsed_version 37 | Versionomy.parse(version_number) 38 | end 39 | 40 | # Return the Ruby method to use when comparing versions 41 | def version_operator_method 42 | VERSION_OPERATORS[version_operator] 43 | end 44 | 45 | # Return the Ruby method to use when comparing times 46 | def time_operator_method 47 | TIME_OPERATORS[time_operator] 48 | end 49 | 50 | # Return the `data` attribute when we serialize the behavior 51 | def as_json(*) 52 | data 53 | end 54 | 55 | # Return a class that acts like a default behavior 56 | class DefaultBehavior < ::Behavior 57 | def self.as_json(*) 58 | { action: 'ok' } 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/killswitch/d747029c477e80aecb94eea1fb60999ad753b83a/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/keyable.rb: -------------------------------------------------------------------------------- 1 | module Keyable 2 | extend ActiveSupport::Concern 3 | KEY_LENGTH = 32 4 | 5 | included do 6 | before_validation :generate_key, if: lambda { key.blank? } 7 | validates :key, presence: true, length: { is: KEY_LENGTH * 2 } 8 | end 9 | 10 | def generate_key 11 | self.key = SecureRandom.hex(KEY_LENGTH) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/concerns/welcomeable.rb: -------------------------------------------------------------------------------- 1 | module Welcomeable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | # Callbacks 6 | after_commit :send_welcome_email!, on: :create, if: lambda { !skip_welcome_email && reset_password_token.blank? } 7 | 8 | # Accessors 9 | attr_accessor :skip_welcome_email 10 | end 11 | 12 | def send_welcome_email! 13 | UserMailer.welcome_email(id, set_reset_password_token).deliver_now 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/membership.rb: -------------------------------------------------------------------------------- 1 | class Membership < ApplicationRecord 2 | # Camaraderie 3 | acts_as_membership 4 | # NOTE: This association is defined by the `acts_as_membership` method above. 5 | # However, we need to redefine it because we add `counter_cache`. 6 | # rubocop:disable Rails/ReflectionClassName 7 | belongs_to :organization, class_name: Camaraderie.organization_class, inverse_of: :memberships, counter_cache: true 8 | # rubocop:enable Rails/ReflectionClassName 9 | 10 | # Callbacks 11 | after_initialize { build_user if new_record? && user.blank? } 12 | before_validation :prepare_user, on: :create 13 | after_validation :restore_user, on: :create, if: lambda { errors.any? } 14 | 15 | # Scopes 16 | scope(:oldest, -> { order(created_at: :asc) }) 17 | 18 | protected 19 | 20 | # If we're trying to save a membership with a user that already 21 | # exists we just use the existing user 22 | def prepare_user 23 | if existing_user = User.find_by(email: user.email) 24 | self.user_id = existing_user.id 25 | else 26 | # Assign a temporary password so the user can be valid 27 | user.password ||= SecureRandom.hex(20) 28 | end 29 | end 30 | 31 | # If the membership is not valid, we revert the user back to 32 | # a non-persisted copy so nested forms will not break 33 | def restore_user 34 | self.user = user.dup 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/models/organization.rb: -------------------------------------------------------------------------------- 1 | class Organization < ApplicationRecord 2 | # Associations 3 | has_many :applications, -> { ascendingly }, dependent: :destroy, inverse_of: :organization 4 | 5 | # FriendlyId 6 | extend FriendlyId 7 | friendly_id :name, use: :slugged 8 | 9 | # Validations 10 | validates :name, presence: true 11 | 12 | # Camaraderie 13 | acts_as_organization 14 | 15 | # Scopes 16 | scope(:ascendingly, -> { order(Arel.sql('lower(name) asc')) }) 17 | end 18 | -------------------------------------------------------------------------------- /app/models/project.rb: -------------------------------------------------------------------------------- 1 | class Project < ApplicationRecord 2 | # Associations 3 | belongs_to :application 4 | has_many :behaviors, -> { ascendingly }, dependent: :destroy, inverse_of: :project 5 | 6 | # Validations 7 | validates :name, presence: true, uniqueness: { scope: [:application_id, :deleted_at] } 8 | 9 | # Concerns 10 | include Keyable 11 | 12 | # FriendlyId 13 | extend FriendlyId 14 | friendly_id :name, use: :scoped, scope: :application 15 | 16 | # PaperTrail 17 | has_paper_trail 18 | 19 | # Scopes 20 | scope(:ascendingly, -> { order(name: :asc) }) 21 | end 22 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | # Validations 3 | validates :name, presence: true 4 | validates :email, presence: true, email: true, uniqueness: { scope: [:deleted_at] } 5 | validates :password, length: { within: 8..128, allow_blank: true }, presence: { if: :password_required? } 6 | 7 | # Devise 8 | devise :database_authenticatable, :rememberable, :trackable, :recoverable, :password_archivable, :session_limitable 9 | 10 | # FriendlyId 11 | extend FriendlyId 12 | friendly_id :name, use: :slugged 13 | 14 | # Camaraderie 15 | acts_as_user 16 | 17 | # PaperTrail 18 | has_paper_trail 19 | 20 | # Concerns 21 | include Welcomeable 22 | 23 | def super_powers? 24 | organizations.exists?(super_admin: true) 25 | end 26 | 27 | def allowed_in?(organization) 28 | allowed_organizations.include?(organization) 29 | end 30 | 31 | def allowed_organizations 32 | @allowed_organizations ||= begin 33 | # Include explicit memberships 34 | organization_ids = memberships.includes(:organization).pluck(:organization_id) 35 | 36 | # Include all organizations if we’re member of an admin organization 37 | organization_ids += Organization.pluck(:id) if super_powers? 38 | 39 | # Get all organizations and order them by name 40 | Organization.where(id: organization_ids.uniq).ascendingly 41 | end 42 | end 43 | 44 | protected 45 | 46 | def password_required? 47 | !persisted? || !password.nil? || !password_confirmation.nil? 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/presenters/application_presenter.rb: -------------------------------------------------------------------------------- 1 | class ApplicationPresenter < Bourgeois::Presenter 2 | end 3 | -------------------------------------------------------------------------------- /app/presenters/behavior_presenter.rb: -------------------------------------------------------------------------------- 1 | class BehaviorPresenter < Bourgeois::Presenter 2 | # Structs 3 | Dropdown = Struct.new(:id, :label) 4 | 5 | def self.version_operators 6 | @_version_operators ||= Behavior::VERSION_OPERATORS.keys.map do |operator| 7 | Dropdown.new(id: operator, label: version_operator_label(operator)) 8 | end 9 | end 10 | 11 | def self.time_operators 12 | @_time_operators ||= Behavior::TIME_OPERATORS.keys.map do |operator| 13 | Dropdown.new(id: operator, label: time_operator_label(operator)) 14 | end 15 | end 16 | 17 | def self.languages 18 | @_languages ||= Behavior::LANGUAGES.map do |language| 19 | Dropdown.new(id: language, label: language_label(language)) 20 | end 21 | end 22 | 23 | def human_language 24 | BehaviorPresenter.language_label(language) 25 | end 26 | 27 | def human_version_operator 28 | BehaviorPresenter.version_operator_label(version_operator) 29 | end 30 | 31 | def human_time_operator 32 | BehaviorPresenter.time_operator_label(time_operator) 33 | end 34 | 35 | def human_version 36 | "#{human_version_operator} #{version_number}" 37 | end 38 | 39 | def human_time 40 | return I18n.t('activerecord.attributes.behavior.empty_time') if time.nil? 41 | 42 | "#{human_time_operator} #{time}" 43 | end 44 | 45 | def pretty_data 46 | JSON.pretty_generate(data) 47 | end 48 | 49 | def action 50 | data['action'] 51 | end 52 | 53 | def self.language_label(language) 54 | I18n.t("activerecord.attributes.behavior.languages.#{language.presence || 'any'}") 55 | end 56 | 57 | def self.version_operator_label(operator) 58 | I18n.t("activerecord.attributes.behavior.version_operators.#{operator}") 59 | end 60 | 61 | def self.time_operator_label(operator) 62 | I18n.t("activerecord.attributes.behavior.time_operators.#{operator}") 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/presenters/membership_presenter.rb: -------------------------------------------------------------------------------- 1 | class MembershipPresenter < Bourgeois::Presenter 2 | # Structs 3 | Dropdown = Struct.new(:id, :label) 4 | 5 | def self.membership_types 6 | @_service_types ||= Camaraderie.membership_types.map do |type| 7 | Dropdown.new(id: type, label: membership_type_label(type)) 8 | end 9 | end 10 | 11 | def human_membership_type 12 | MembershipPresenter.membership_type_label(membership_type) 13 | end 14 | 15 | def self.membership_type_label(type) 16 | I18n.t("activerecord.attributes.membership.membership_types.#{type}") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/presenters/organization_presenter.rb: -------------------------------------------------------------------------------- 1 | class OrganizationPresenter < Bourgeois::Presenter 2 | end 3 | -------------------------------------------------------------------------------- /app/presenters/project_presenter.rb: -------------------------------------------------------------------------------- 1 | class ProjectPresenter < Bourgeois::Presenter 2 | def curl_example(opts = {}) 3 | url_opts = opts.merge!(key:, version: '_VERSION_') 4 | api_url = view.api_url(url_opts).gsub('_VERSION_', '$version') 5 | 6 | %(echo -n 'Enter a version number: '; read version; curl -H "Accept-Language: #{I18n.locale}" "#{api_url}") 7 | end 8 | 9 | def full_name 10 | "#{view.present(application).name} / #{name}" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/presenters/user_presenter.rb: -------------------------------------------------------------------------------- 1 | class UserPresenter < Bourgeois::Presenter 2 | end 3 | -------------------------------------------------------------------------------- /app/services/ability.rb: -------------------------------------------------------------------------------- 1 | class Ability 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | @user = user || User.new 6 | 7 | if @user.super_powers? 8 | # We can do everything! 9 | can :manage, :all 10 | 11 | # Prevent from destroying super-admin organizations 12 | cannot :destroy, Organization, &:super_admin? 13 | else 14 | organization_permissions 15 | end 16 | 17 | # Prevent users from removing themselves from organizations 18 | cannot(:destroy, Membership) do |membership| 19 | membership.user == user 20 | end 21 | end 22 | 23 | protected 24 | 25 | def organization_permissions 26 | can(:access, Organization) { |organization| @user.allowed_in?(organization) } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/services/behavior_dispatcher.rb: -------------------------------------------------------------------------------- 1 | class BehaviorDispatcher 2 | attr_reader :matching_behavior 3 | 4 | class MissingParameter < StandardError 5 | end 6 | 7 | def dispatch!(request) 8 | @request = request 9 | 10 | fetch_version 11 | fetch_key 12 | fetch_project 13 | fetch_language 14 | 15 | @matching_behavior = @project.behaviors.find { |b| matches?(b) } 16 | @matching_behavior ||= Behavior::DefaultBehavior 17 | end 18 | 19 | protected 20 | 21 | def fetch_version 22 | @version = Versionomy.parse(@request.params[:version]) 23 | rescue StandardError 24 | raise MissingParameter, 'Missing or invalid “version” parameter' 25 | end 26 | 27 | def fetch_key 28 | @key = @request.params[:key] 29 | raise MissingParameter, 'Missing or invalid “key” parameter' if @key.blank? 30 | end 31 | 32 | def fetch_project 33 | @project = Project.where(key: @key).includes(:behaviors).first! 34 | end 35 | 36 | def fetch_language 37 | available_languages = @project.behaviors.pluck(:language).compact.uniq 38 | return unless available_languages.any? 39 | 40 | language_matcher = if params[:http_accept_language].present? 41 | Rack::Accept::Language.new(params[:http_accept_language]) 42 | else 43 | @request.env['rack-accept.request'].language 44 | end 45 | 46 | language_matcher.first_level_match = true 47 | @language = language_matcher.best_of(available_languages) 48 | end 49 | 50 | # Return true only if all our conditions are true 51 | def matches?(behavior) 52 | conditions = [ 53 | version_matches?(behavior), 54 | language_matches?(behavior), 55 | time_matches?(behavior) 56 | ] 57 | 58 | conditions.exclude?(false) 59 | end 60 | 61 | # Return true if our version matches 62 | def version_matches?(behavior) 63 | # Eg. '1.5.0'.send(:<=, '1.4.0') # => false 64 | @version.send(behavior.version_operator_method, behavior.parsed_version) 65 | end 66 | 67 | # Return true if our time matches 68 | def time_matches?(behavior) 69 | return true if behavior.time.blank? 70 | 71 | Time.zone.now.utc.public_send(behavior.time_operator_method, behavior.time) 72 | end 73 | 74 | # Return true if our language matches 75 | def language_matches?(behavior) 76 | behavior.language.nil? || @language == behavior.language 77 | end 78 | 79 | def params 80 | @params ||= @request.env['action_dispatch.request.parameters'].with_indifferent_access 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /app/services/behavior_sorter.rb: -------------------------------------------------------------------------------- 1 | class BehaviorSorter 2 | def initialize(project) 3 | @project = project 4 | end 5 | 6 | def reorder!(behavior_ids) 7 | @behavior_ids = behavior_ids 8 | 9 | sanitize_behaviors 10 | reset_all_orders 11 | reorder_each_behavior 12 | 13 | true 14 | end 15 | 16 | protected 17 | 18 | # Convert the array to a { id1 => 0, id2 => 1 } hash 19 | def sanitize_behaviors 20 | @behaviors_map = @behavior_ids.each_with_index.to_a.to_h 21 | end 22 | 23 | # Apply new order to each behavior 24 | def reorder_each_behavior 25 | behaviors = @project.behaviors.where(id: @behavior_ids) 26 | behaviors.each do |behavior| 27 | behavior.update behavior_order: @behaviors_map[behavior.id.to_s] 28 | end 29 | end 30 | 31 | # Reset all order keys for the project 32 | # rubocop:disable Rails/SkipsModelValidations 33 | def reset_all_orders 34 | @project.behaviors.update_all behavior_order: 0 35 | end 36 | # rubocop:enable Rails/SkipsModelValidations 37 | end 38 | -------------------------------------------------------------------------------- /app/tasks/sample_data.rake: -------------------------------------------------------------------------------- 1 | namespace :sample_data do 2 | task create: :environment do 3 | Rails.logger = Logger.new(STDOUT) 4 | 5 | # Example organization 6 | example_organization = Organization.where(name: 'Example inc.', super_admin: true).first_or_create 7 | 8 | # Admin user 9 | email = ENV["KILLSWITCH_SAMPLE_DATA_EMAIL"] or raise "KILLSWITCH_SAMPLE_DATA_EMAIL is not set" 10 | password = ENV["KILLSWITCH_SAMPLE_DATA_PASSWORD"] or raise "KILLSWITCH_SAMPLE_DATA_PASSWORD is not set" 11 | Rails.logger.info "Admin email: #{email}" 12 | Rails.logger.info "Admin password: #{password}" 13 | admin_user = User.where(name: email, email: email).first_or_create(password: password) 14 | example_organization.admins.create(user: admin_user) 15 | 16 | # First application 17 | app1 = example_organization.applications.where(name: 'First project').first_or_create 18 | app1.projects.where(name: 'Development').create 19 | app1.projects.where(name: 'Staging').create 20 | app1.projects.where(name: 'Production').create 21 | 22 | # Second application 23 | app2 = example_organization.applications.where(name: 'Second application').first_or_create 24 | app2.projects.where(name: 'Development').create 25 | app2.projects.where(name: 'Staging').create 26 | app2.projects.where(name: 'Production').create 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/utilities/active_record_configuration_override.rb: -------------------------------------------------------------------------------- 1 | class ActiveRecordConfigurationOverride 2 | def self.override! 3 | # Reset ActiveRecord logger 4 | ActiveRecord::Base.logger = Rails.logger 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/utilities/asset_host.rb: -------------------------------------------------------------------------------- 1 | class AssetHost 2 | def initialize(configuration) 3 | @host = configuration.domain 4 | @port = configuration.port 5 | @protocol = configuration.protocol 6 | 7 | options = { host: @host, scheme: @protocol } 8 | options[:port] = @port if @port.present? 9 | 10 | @uri = URI::Generic.build(options) 11 | end 12 | 13 | delegate :to_s, to: :@uri 14 | end 15 | -------------------------------------------------------------------------------- /app/utilities/boolean_environment_variable.rb: -------------------------------------------------------------------------------- 1 | class BooleanEnvironmentVariable 2 | def initialize(value) 3 | @value = value 4 | end 5 | 6 | def as_bool 7 | [nil, '', '0', 'false'].exclude?(@value.try(:downcase)) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/validators/email_validator.rb: -------------------------------------------------------------------------------- 1 | class EmailValidator < ActiveModel::EachValidator 2 | def initialize(options) 3 | options.reverse_merge!(message: :invalid_email) 4 | options.reverse_merge!(regex: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i) 5 | super 6 | end 7 | 8 | def validate_each(record, attribute, value) 9 | return if value =~ options.fetch(:regex) 10 | 11 | record.errors.add(attribute, options.fetch(:message), value:) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/validators/version_validator.rb: -------------------------------------------------------------------------------- 1 | class VersionValidator < ActiveModel::EachValidator 2 | def initialize(options) 3 | options.reverse_merge!(message: :invalid_version) 4 | super 5 | end 6 | 7 | def validate_each(record, attribute, value) 8 | if value.blank? 9 | add_error(record, attribute, value) 10 | else 11 | begin 12 | Versionomy.parse(value) 13 | rescue Versionomy::Errors::ParseError 14 | add_error(record, attribute, value) 15 | end 16 | end 17 | end 18 | 19 | protected 20 | 21 | def add_error(record, attribute, value) 22 | record.errors.add(attribute, options.fetch(:message), value:) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.en.html.erb: -------------------------------------------------------------------------------- 1 |
Hello <%= @resource.name %>,
2 | 3 |
Someone has requested a link to change your password. You can do this through the link below.
4 |
<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token), style: inline_styles(:'color' => '#4288cb') %>
5 | 6 |
If you didn’t request this, please ignore this email.
7 |
Your password won’t change until you access the link above and create a new one.
8 | -------------------------------------------------------------------------------- /app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t('.intro') %>

3 |
4 | 5 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: 'put' }) do |form| %> 6 | <%= devise_error_messages! %> 7 | <%= form.hidden_field :reset_password_token %> 8 | 9 |
10 | <%= form.label :password %> 11 | <%= form.password_field :password, class: 'form-control' %> 12 |
13 | 14 |

15 | <%= button_tag icon(:save, t('.save')), class: 'btn btn-primary' %> 16 |

17 | <% end %> 18 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t('.intro') %>

3 |
4 | 5 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: 'post' }) do |form| %> 6 | <%= devise_error_messages! %> 7 | 8 |
9 | <%= form.label :email %> 10 | <%= form.email_field :email, class: 'form-control' %> 11 |
12 | 13 |

14 | <%= button_tag icon(:envelope_o, t('.send_instructions')), class: 'btn btn-primary' %> 15 |

16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for resource, as: resource_name, url: session_path(resource_name) do |form| %> 2 | <%= devise_error_messages! %> 3 | 4 |
5 | <%= form.label :email %> 6 | <%= form.email_field :email, autofocus: true, class: 'form-control' %> 7 |
8 | 9 |
10 | <%= form.label :password %> 11 | <%= form.password_field :password, class: 'form-control' %> 12 |
13 | 14 |
15 | <%= label_tag do %> 16 | <%= form.check_box :remember_me %> 17 | <%= t('.remember_me') %> 18 | <% end %> 19 |
20 | 21 |

22 | <%= button_tag icon(:check, t('.sign_in')), class: 'btn btn-primary' %> 23 | <%= link_to t('.forgot_password'), new_password_path(resource_name), class: 'btn' %> 24 |

25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= page_title(base: t('.base_title')) %> 5 | 6 | <%= stylesheet_link_tag 'application' %> 7 | <%= javascript_include_tag 'vendor' %> 8 | <%= javascript_include_tag 'application' %> 9 | <%= csrf_meta_tags %> 10 | <%= favicon_link_tag 'img-favicon.png' %> 11 | 12 | 13 | <% unless content_for(:hide_navigation) %> 14 | 70 |
71 | <%= render partial: 'shared/flashes' %> 72 | <%= yield %> 73 |
74 | <% else %> 75 | <%= yield %> 76 | <% end %> 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 75 | 76 | 77 |
  12 | 13 | 14 | 15 | 16 | 17 | 28 | 29 | 30 | 49 | 50 | 51 | 72 | 73 |
 
18 | 19 | 20 | 21 | 24 | 25 | 26 |
22 | <%= link_to image_tag('img-favicon.png', width: 65, border: 0), root_url %> 23 |
27 |
31 | 32 | 33 | 34 | 45 | 46 | 47 |
35 | 36 | 37 | 38 | 41 | 42 | 43 |
  39 | <%= yield %> 40 |  
44 |
48 |
52 | 53 | 54 | 55 | 68 | 69 | 70 |
56 | 57 | 58 | 59 | 64 | 65 | 66 |
60 |
61 | <%= t('.footer_html', url: root_url, style: inline_styles(:'color' => '#888888', :'font-size' => '11px')) %> 62 |
63 |
67 |
71 |
74 |
 
78 | 79 | 80 | -------------------------------------------------------------------------------- /app/views/shared/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t('.message') %>

3 | 8 |
9 | -------------------------------------------------------------------------------- /app/views/shared/_exception_backtrace.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

<%= request.env['action_dispatch.exception'] %>

5 |

<%= request.env['action_dispatch.exception'].inspect %>

6 | 7 |

<%= t('.parameters') %>

8 |
<%= params.inspect %>
9 | 10 |

<%= t('.backtrace') %>

11 |
<%= pretty_backtrace(request.env['action_dispatch.exception']) %>
12 |
13 | -------------------------------------------------------------------------------- /app/views/shared/_flashes.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash[:notice] %> 2 |
3 | <%= flash[:notice] %> 4 |
5 | <% end %> 6 | 7 | <% if flash[:alert] %> 8 |
9 | <%= flash[:alert] %> 10 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /app/views/shared/_logo.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/views/user_mailer/welcome_email.en.html.erb: -------------------------------------------------------------------------------- 1 |
Welcome to the Killswitch, <%= @user.name %>!
2 |
Killswitch is a clever control panel built by Mirego that allows mobile developers to apply runtime version-specific behaviors to their iOS or Android application.
3 |
You just need to <%= link_to 'set your password', edit_password_url(@user, reset_password_token: @token), style: inline_styles(:'color' => '#4288cb') %> and you will be able to access the admin panel.
4 | -------------------------------------------------------------------------------- /app/views/web/applications/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_error_messages(form.object) %> 2 | 3 |
4 | <%= form.label :name %> 5 | <%= form.text_field :name, class: 'form-control', autofocus: true %> 6 |
7 | -------------------------------------------------------------------------------- /app/views/web/applications/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.edit_application', application: present(@application).name) %> 3 | 4 |

<%= t('.edit_application', application: present(@application).name) %>

5 | 6 | <%= form_for @application, url: web_organization_application_path do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

10 | <%= button_tag icon(:save, t('.save')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organization_application_path, class: 'btn btn-default' %> 12 |

13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/applications/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.page_title') %> 3 | 4 | <% if @applications.any? %> 5 | 21 | <% else %> 22 |
<%= t('.empty_applications') %>
23 | <% end %> 24 | 25 |
26 | 27 | <%= link_to icon(:plus, t('.add_application')), new_web_organization_application_path, class: 'btn btn-primary' %> 28 | -------------------------------------------------------------------------------- /app/views/web/applications/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.add_application') %> 3 | 4 |

<%= t('.add_application') %>

5 | 6 | <%= form_for @application, url: web_organization_applications_path do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

10 | <%= button_tag icon(:plus, t('.create')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organization_applications_path, class: 'btn btn-default' %> 12 |

13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/applications/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.page_title', application: present(@application).name) %> 3 | 4 | <% present @application do |application| %> 5 |

<%= t('.page_title', application: present(application).name) %>

6 | 7 | <% present(application.projects) do |project| %> 8 | <%= link_to project.name, web_organization_application_project_path(application_id: application, id: project), class: 'btn' %> 9 | <% end %> 10 | 11 | <%= link_to icon(:plus), new_web_organization_application_project_path(application_id: @application), class: 'btn btn-primary btn-sm' %> 12 | <% end %> 13 | 14 |
15 | 16 | <%= link_to icon(:pencil, t('.edit')), edit_web_organization_application_path(id: @application), class: 'btn btn-primary' %> 17 | <%= link_to icon(:trash_o), web_organization_application_path(id: @application), class: 'btn btn-default', data: { method: 'delete', confirm: t('.confirm_destroy') } %> 18 | -------------------------------------------------------------------------------- /app/views/web/behaviors/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_error_messages(form.object) %> 2 | 3 |
4 |
5 |
6 | <%= form.label :version_operator %> 7 | <%= form.collection_select :version_operator, BehaviorPresenter.version_operators, :id, :label, { prompt: t('.please_select') }, { class: 'form-control' } %> 8 |
9 |
10 | 11 |
12 |
13 | <%= form.label :version_number %> 14 | <%= form.text_field :version_number, class: 'form-control', autofocus: true %> 15 |
16 |
17 | 18 |
19 |
20 | <%= form.label :time_operator %> 21 | <%= form.collection_select :time_operator, BehaviorPresenter.time_operators, :id, :label, { prompt: t('.none') }, { class: 'form-control' } %> 22 |
23 |
24 | 25 |
26 |
27 | <%= form.label :time %> (UTC) 28 | <%= form.datetime_local_field :time, class: 'form-control' %> 29 |
30 |
31 | 32 |
33 |
34 | <%= form.label :language %> 35 | <%= form.collection_select :language, BehaviorPresenter.languages, :id, :label, { prompt: t('.any_language') }, { class: 'form-control' } %> 36 |
37 |
38 |
39 | 40 |
41 | <%= form.label :data %> 42 | 43 |
44 | 47 | 48 | 53 |
54 | 55 | <%= form.text_area :data, class: 'form-control fixed-width-font', rows: 14, value: form.object.data_invalid_json.presence || form.object.pretty_data %> 56 |
57 | -------------------------------------------------------------------------------- /app/views/web/behaviors/_mini.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    4 | <%= Behavior.human_attribute_name(:version) %>: 5 | <%= behavior.human_version %> 6 |
    7 | 8 |
    9 | <%= Behavior.human_attribute_name(:time) %>: 10 | <%= behavior.human_time %> 11 |
    12 | 13 |
    14 | <%= Behavior.human_attribute_name(:language) %>: 15 | <%= behavior.human_language %> 16 |
    17 | 18 |
    19 | <%= Behavior.human_attribute_name(:action) %>: 20 | <%= behavior.action %> 21 |
    22 | 23 |
    24 | <%= Behavior.human_attribute_name(:data) %>: 25 | <%= link_to t('.toggle'), 'javascript://', class: 'toggle btn btn-xs btn-link' %> 26 |
    27 | 28 |
    29 | <%= link_to icon(:pencil), edit_web_organization_application_project_behavior_path(application_id: @application, project_id: @project, id: behavior), class: 'btn btn-primary btn-xs' %> 30 | <%= link_to icon(:trash_o), web_organization_application_project_behavior_path(application_id: @application, project_id: @project, id: behavior), class: 'btn btn-default btn-xs', data: { method: :delete, confirm: t('.confirm_destroy') } %> 31 |
    32 |
    33 | 34 |
    35 |
    <%= behavior.pretty_data %>
    36 |
    37 |
  • 38 | -------------------------------------------------------------------------------- /app/views/web/behaviors/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.edit_behavior', project: present(@behavior.project).full_name) %> 3 | 4 |

    <%= t('.edit_behavior', project: present(@behavior.project).full_name) %>

    5 | 6 | <%= form_for present(@behavior), url: web_organization_application_project_behavior_path do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

    10 | <%= button_tag icon(:save, t('.save')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organization_application_project_path(application_id: @application, id: @project), class: 'btn btn-default' %> 12 |

    13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/behaviors/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.add_behavior', project: present(@behavior.project).full_name) %> 3 | 4 |

    <%= t('.add_behavior', project: present(@behavior.project).full_name) %>

    5 | 6 | <%= form_for present(@behavior), url: web_organization_application_project_behaviors_path do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

    10 | <%= button_tag icon(:plus, t('.create')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organization_application_project_path(application_id: form.object.application, id: form.object.project), class: 'btn btn-default' %> 12 |

    13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/errors/forbidden.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= t('.title') %>

    2 |

    <%= t('.content') %>

    3 | 4 | <% if Rails.application.secrets.show_backtrace %> 5 | <%= render partial: 'shared/exception_backtrace' %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/web/errors/internal_server_error.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= t('.title') %>

    2 |

    <%= t('.content') %>

    3 | 4 | <% if Rails.application.secrets.show_backtrace %> 5 | <%= render partial: 'shared/exception_backtrace' %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/web/errors/not_found.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= t('.title') %>

    2 |

    <%= t('.content') %>

    3 | 4 | <% if Rails.application.secrets.show_backtrace %> 5 | <%= render partial: 'shared/exception_backtrace' %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/web/home/show.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_user %> 2 | <%= t('.orphan') %> 3 | <% else %> 4 | <% content_for(:hide_navigation, true) %> 5 | 6 |
    7 |
    8 | <%= render partial: 'shared/flashes' %> 9 | 10 | <%= link_to root_path, class: 'homepage-logo' do %> 11 | <%= logo(class: 'homepage-logo-image') %> 12 | <% end %> 13 | 14 |
    15 | <%= t('.intro') %> 16 |
    17 | 18 | <%= link_to icon(:user, t('.sign_in')), new_user_session_path, class: 'btn btn-primary' %> 19 | 20 |
    <%= version %>
    21 |
    22 |
    23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/web/memberships/_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= form.label :membership_type %> 3 | <%= form.collection_select :membership_type, MembershipPresenter.membership_types, :id, :label, { prompt: false }, { class: 'form-control' } %> 4 |
    5 | -------------------------------------------------------------------------------- /app/views/web/memberships/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :users %> 2 | <% content_for :page_title, t('.edit_user', user: present(@membership.user).name) %> 3 | 4 |

    <%= t('.edit_user', user: present(@membership.user).name) %>

    5 | 6 | <%= form_for @membership, url: web_organization_membership_path do |form| %> 7 | <%= render_error_messages(form.object) %> 8 | 9 | <%= form.fields_for :user do |form_user| %> 10 |
    11 | <%= form_user.label :name %> 12 |

    <%= form_user.object.send(:name) %>

    13 |
    14 | 15 |
    16 | <%= form_user.label :email %> 17 |

    <%= form_user.object.send(:email) %>

    18 |
    19 | <% end %> 20 | 21 | <%= render partial: 'form', locals: { form: form } %> 22 | 23 |

    24 | <%= button_tag icon(:save, t('.save')), class: 'btn btn-primary' %> 25 | <%= link_to t('.cancel'), web_organization_memberships_path, class: 'btn btn-default' %> 26 |

    27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/views/web/memberships/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :users %> 2 | <% content_for :page_title, t('.page_title', organization: @organization.name) %> 3 | 4 |

    <%= t('.page_title', organization: @organization.name) %>

    5 | 6 | <% if @memberships.any? %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% present @memberships do |membership| %> 16 | 17 | 18 | <% present membership.user do |user| %> 19 | 20 | 21 | <% end %> 22 | 23 | 29 | 30 | <% end %> 31 |
    <%= Membership.human_attribute_name(:id) %><%= Membership.human_attribute_name(:name) %><%= Membership.human_attribute_name(:email) %><%= Membership.human_attribute_name(:membership_type) %>
    <%= membership.user.id %><%= user.name %><%= user.email %><%= membership.human_membership_type %> 24 | <%= link_to icon(:pencil), edit_web_organization_membership_path(id: membership), class: 'btn btn-primary btn-xs' %> 25 | <% if can?(:destroy, membership) %> 26 | <%= link_to icon(:trash_o), web_organization_membership_path(id: membership), class: 'btn btn-default btn-xs', data: { method: :delete, confirm: t('.confirm_destroy') } %> 27 | <% end %> 28 |
    32 | <% else %> 33 |
    <%= t('.empty_users') %>
    34 | <% end %> 35 | 36 |
    37 | 38 | <%= link_to icon(:plus, t('.add_user')), new_web_organization_membership_path, class: 'btn btn-primary' %> 39 | -------------------------------------------------------------------------------- /app/views/web/memberships/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :users %> 2 | <% content_for :page_title, t('.add_user') %> 3 | 4 |

    <%= t('.add_user') %>

    5 | 6 | <%= form_for @membership, url: web_organization_memberships_path do |form| %> 7 | <%= render_error_messages(form.object) %> 8 | 9 | <%= form.fields_for :user do |form_user| %> 10 |
    11 | <%= form_user.label :name %> 12 | <%= form_user.text_field :name, class: 'form-control', autofocus: true %> 13 |
    14 | 15 |
    16 | <%= form_user.label :email %> 17 | <%= form_user.email_field :email, class: 'form-control' %> 18 |
    19 | <% end %> 20 | 21 | <%= render partial: 'form', locals: { form: form } %> 22 | 23 |

    24 | <%= button_tag icon(:plus, t('.create')), class: 'btn btn-primary' %> 25 | <%= link_to t('.cancel'), web_organization_memberships_path, class: 'btn btn-default' %> 26 |

    27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/views/web/organizations/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_error_messages(form.object) %> 2 | 3 |
    4 | <%= form.label :name %> 5 | <%= form.text_field :name, class: 'form-control', autofocus: true %> 6 |
    7 | -------------------------------------------------------------------------------- /app/views/web/organizations/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :users %> 2 | <% content_for :page_title, t('.edit_organization', organization: present(@organization).name) %> 3 | 4 |

    <%= t('.edit_organization', organization: present(@organization).name) %>

    5 | 6 | <%= form_for @organization, url: web_organization_path(@organization) do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

    10 | <%= button_tag icon(:plus, t('.save')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organizations_path, class: 'btn btn-default' %> 12 |

    13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/organizations/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title, t('.page_title') %> 2 | 3 |

    <%= t('.page_title') %>

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% present @organizations do |organization| %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | <% end %> 29 |
    <%= Organization.human_attribute_name(:id) %><%= Organization.human_attribute_name(:name) %><%= Organization.human_attribute_name(:applications_count) %><%= Organization.human_attribute_name(:memberships_count) %><%= Organization.human_attribute_name(:admin) %>
    <%= organization.id %><%= organization.name %><%= link_to organization.applications_count, web_organization_applications_path(organization) %><%= link_to organization.memberships_count, web_organization_memberships_path(organization) %><%= bool_icon(organization.super_admin?) %> 22 | <%= link_to icon(:pencil), edit_web_organization_path(organization), class: 'btn btn-primary btn-xs' %> 23 | <% if can?(:destroy, organization) %> 24 | <%= link_to icon(:trash_o), web_organization_path(organization), class: 'btn btn-default btn-xs', data: { method: :delete, confirm: t('.confirm_destroy') } %> 25 | <% end %> 26 |
    30 | 31 |
    32 | 33 | <%= link_to icon(:plus, t('.add_organization')), new_web_organization_path, class: 'btn btn-primary' %> 34 | -------------------------------------------------------------------------------- /app/views/web/organizations/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :users %> 2 | <% content_for :page_title, t('.add_organization') %> 3 | 4 |

    <%= t('.add_organization') %>

    5 | 6 | <%= form_for @organization, url: web_organizations_path do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

    10 | <%= button_tag icon(:plus, t('.create')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organizations_path, class: 'btn btn-default' %> 12 |

    13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/projects/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_error_messages(form.object) %> 2 | 3 |
    4 | <%= form.label :name %> 5 | <%= form.text_field :name, class: 'form-control', autofocus: true %> 6 |
    7 | -------------------------------------------------------------------------------- /app/views/web/projects/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.edit_project', project: present(@project).full_name) %> 3 | 4 |

    <%= t('.edit_project', project: present(@project).full_name) %>

    5 | 6 | <%= form_for @project, url: web_organization_application_project_path do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

    10 | <%= button_tag icon(:save, t('.save')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organization_application_project_path, class: 'btn btn-default' %> 12 |

    13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/projects/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.add_project', application: present(@project.application).name) %> 3 | 4 |

    <%= t('.add_project', application: present(@project.application).name) %>

    5 | 6 | <%= form_for @project, url: web_organization_application_projects_path do |form| %> 7 | <%= render partial: 'form', locals: { form: form } %> 8 | 9 |

    10 | <%= button_tag icon(:plus, t('.create')), class: 'btn btn-primary' %> 11 | <%= link_to t('.cancel'), web_organization_application_path(id: form.object.application), class: 'btn btn-default' %> 12 |

    13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/web/projects/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav_active_item, :applications %> 2 | <% content_for :page_title, t('.page_title', project: present(@project).full_name) %> 3 | 4 | <% present @project do |project| %> 5 |

    <%= project.full_name %>

    6 | 7 |

    <%= t('.informations') %>

    8 | 9 | 10 | 11 | 12 | 13 |
    <%= Project.human_attribute_name(:key) %><%= project.key %>
    14 | 15 |

    16 | <%= Project.human_attribute_name(:behaviors) %> 17 | <%= link_to icon(:plus), new_web_organization_application_project_behavior_path(application_id: @application, project_id: project), class: 'btn btn-primary btn-xs' %> 18 |

    19 | 20 | <% if project.behaviors.any? %> 21 | 26 | <% else %> 27 |
    <%= t('.empty_behaviors') %>
    28 | <% end %> 29 | 30 |

    <%= t('.example') %>

    31 |
    <%= project.curl_example %>
    32 | 33 |
    34 | 35 | <%= link_to icon(:pencil, t('.edit')), edit_web_organization_application_project_path(application_id: @application, id: project), class: 'btn btn-primary' %> 36 | <%= link_to icon(:trash_o), web_organization_application_project_path(application_id: @application, id: project), class: 'btn btn-default', data: { method: 'delete', confirm: t('.confirm_destroy') } %> 37 | <% end %> 38 | -------------------------------------------------------------------------------- /app/views/web/users/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_error_messages(form.object) %> 2 | 3 |
    4 | <%= form.label :name %> 5 | <%= form.text_field :name, class: 'form-control', autofocus: true %> 6 |
    7 | 8 |
    9 | <%= form.label :email %> 10 | <%= form.email_field :email, class: 'form-control' %> 11 |
    12 | 13 |
    14 | <%= form.label :password %> (leave blank to remain unchanged) 15 | <%= form.password_field :password, class: 'form-control' %> 16 |
    17 | -------------------------------------------------------------------------------- /app/views/web/users/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title, t('.edit_account', user: present(@user).name) %> 2 | 3 |

    <%= t('.edit_account', user: present(@user).name) %>

    4 | 5 | <%= form_for @user, url: web_user_path do |form| %> 6 | <%= render partial: 'form', locals: { form: form } %> 7 | 8 |

    9 | <%= button_tag icon(:save, t('.save')), class: 'btn btn-primary' %> 10 | <%= link_to t('.cancel'), root_path, class: 'btn btn-default' %> 11 |

    12 | <% end %> 13 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require 'bundler/setup' 7 | load Gem.bin_path('rake', 'rake') 8 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require 'bundler/setup' 7 | load Gem.bin_path('rspec', 'rspec') 8 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast 4 | # It gets overwritten when you run the `spring binstub` command 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) 11 | ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) 12 | ENV["GEM_HOME"] = "" 13 | Gem.paths = ENV 14 | 15 | gem "spring", match[1] 16 | require "spring/binstub" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require File.expand_path('../config/environment', __FILE__) 4 | use HealthChecks 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | require File.expand_path('../../app/utilities/asset_host', __FILE__) 5 | require File.expand_path('../../app/utilities/boolean_environment_variable', __FILE__) 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(:default, Rails.env) 10 | 11 | # rubocop:disable Style/IfUnlessModifier 12 | module Killswitch 13 | class Application < Rails::Application 14 | # Version 15 | VERSION = '1.3.1'.freeze 16 | 17 | config.load_defaults 6.0 18 | 19 | # English! 20 | config.i18n.default_locale = :en 21 | 22 | # Do not wrap erroenous form fields with a div 23 | # rubocop:disable Rails/OutputSafety 24 | config.action_view.field_error_proc = lambda { |html_tag, _| html_tag.to_s.html_safe } 25 | # rubocop:enable Rails/OutputSafety 26 | 27 | # Custom exceptions 28 | config.action_dispatch.rescue_responses['BehaviorDispatcher::MissingParameter'] = :bad_request 29 | config.action_dispatch.rescue_responses['CanCan::AccessDenied'] = :forbidden 30 | 31 | # Force SSL on everything except '/killswitch' endpoint 32 | if Rails.application.secrets.force_ssl 33 | config.middleware.use Rack::SSL, exclude: lambda { |env| Rack::Request.new(env).path == '/killswitch' } 34 | end 35 | 36 | # Rack::Cors 37 | config.middleware.insert_before 0, Rack::Cors do 38 | allow do 39 | origins '*' 40 | resource '*', headers: :any, methods: %i(get) 41 | end 42 | end 43 | 44 | # Rack::Accept 45 | config.middleware.use Rack::Accept 46 | 47 | # Canonical host 48 | if Rails.application.secrets.domain 49 | config.middleware.use Rack::CanonicalHost, Rails.application.secrets.domain 50 | end 51 | 52 | # Basic Auth 53 | if Rails.application.secrets.auth_username && Rails.application.secrets.auth_password 54 | config.middleware.use Rack::Auth::Basic, 'Protected Area' do |username, password| 55 | username == Rails.application.secrets.auth_username && password == Rails.application.secrets.auth_password 56 | end 57 | end 58 | 59 | # Mailers 60 | config.action_mailer.default_url_options = { host: Rails.application.secrets.domain, port: Rails.application.secrets.port } 61 | config.action_mailer.asset_host = AssetHost.new(Rails.application.secrets).to_s 62 | 63 | # SMTP server 64 | config.action_mailer.delivery_method = :smtp 65 | config.action_mailer.smtp_settings = { 66 | address: Rails.application.secrets.smtp_address, 67 | port: Rails.application.secrets.smtp_port, 68 | user_name: Rails.application.secrets.smtp_username, 69 | password: Rails.application.secrets.smtp_password 70 | } 71 | end 72 | end 73 | # rubocop:enable Style/IfUnlessModifier 74 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'logger' # Fix concurrent-ruby removing logger dependency which Rails itself does not have 5 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 6 | -------------------------------------------------------------------------------- /config/brakeman.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :skip_checks: 3 | - CheckCookieSerialization 4 | :pager: false 5 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: postgresql 3 | database: killswitch_development 4 | host: 127.0.0.1 5 | 6 | test: 7 | adapter: postgresql 8 | database: killswitch_test 9 | host: 127.0.0.1 10 | 11 | production: 12 | adapter: postgresql 13 | database: killswitch_production 14 | host: 127.0.0.1 15 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Killswitch::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Killswitch::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = false 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Raise an error when translation is missing 31 | config.i18n.raise_on_missing_translations = true 32 | end 33 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Killswitch::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # Specifies the header that your server uses for sending files. 36 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 37 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 38 | 39 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 40 | # config.force_ssl = true 41 | 42 | # Set to :debug to see everything in the log. 43 | config.log_level = :info 44 | 45 | # Prepend all log lines with the following tags. 46 | # config.log_tags = [ :subdomain, :uuid ] 47 | 48 | # Use a different logger for distributed setups. 49 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 50 | 51 | # Use a different cache store in production. 52 | # config.cache_store = :mem_cache_store 53 | 54 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 55 | # config.action_controller.asset_host = "http://assets.example.com" 56 | 57 | # Precompile additional assets. 58 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 59 | # config.assets.precompile += %w( search.js ) 60 | 61 | # Ignore bad email addresses and do not raise email delivery errors. 62 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 63 | # config.action_mailer.raise_delivery_errors = false 64 | 65 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 66 | # the I18n.default_locale when a translation can not be found). 67 | config.i18n.fallbacks = true 68 | 69 | # Send deprecation notices to registered listeners. 70 | config.active_support.deprecation = :notify 71 | 72 | # Disable automatic flushing of the log to improve performance. 73 | # config.autoflush_log = false 74 | 75 | # Use default logging formatter so that PID and timestamp are not suppressed. 76 | config.log_formatter = Logger::Formatter.new 77 | end 78 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Killswitch::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = false 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = true 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Do not use a canonical host during tests 38 | config.middleware.delete Rack::CanonicalHost 39 | end 40 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | 16 | # Make sure we precompile vendor font assets 17 | Rails.application.config.assets.paths << Rails.root.join('vendor/assets/fonts') 18 | Rails.application.config.assets.precompile << /\.(?:svg|eot|woff|ttf)$/ 19 | 20 | # Make sure we compile other top-level assets 21 | Rails.application.config.assets.precompile += %w( 22 | vendor.js 23 | ) 24 | 25 | # Include NPM components 26 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 27 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/camaraderie.rb: -------------------------------------------------------------------------------- 1 | Camaraderie.configure do |config| 2 | # The different types of memberships 3 | config.membership_types = %w(admin) 4 | 5 | # The class name of the organization model 6 | config.organization_class = 'Organization' 7 | 8 | # The class name of the user model 9 | config.user_class = 'User' 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | Rails.application.config.content_security_policy do |policy| 8 | policy.default_src :self, :https 9 | policy.font_src :self, :https, :data 10 | policy.img_src :self, :https, :data 11 | policy.object_src :none 12 | policy.script_src :self, :https 13 | policy.style_src :self, :https 14 | 15 | # Specify URI for violation reports 16 | # policy.report_uri "/csp-violation-report-endpoint" 17 | end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Report CSP violations to a specified URI 23 | # For further information see the following documentation: 24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # Rails.application.config.content_security_policy_report_only = true 26 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :hybrid 6 | -------------------------------------------------------------------------------- /config/initializers/devise_security.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Devise.setup do |config| 4 | # ==> Security Extension 5 | # Configure security extension for devise 6 | 7 | # Should the password expire (e.g 3.months) 8 | # config.expire_password_after = false 9 | 10 | # Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol 11 | # You may use "digits" in place of "digit" and "symbols" in place of 12 | # "symbol" based on your preference 13 | # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 } 14 | 15 | # How many passwords to keep in archive 16 | config.password_archiving_count = 5 17 | 18 | # Deny old passwords (true, false, number_of_old_passwords_to_check) 19 | # Examples: 20 | # config.deny_old_passwords = false # allow old passwords 21 | # config.deny_old_passwords = true # will deny all the old passwords 22 | # config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords 23 | # config.deny_old_passwords = true 24 | 25 | # enable email validation for :secure_validatable. (true, false, validation_options) 26 | # dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation 27 | # config.email_validation = true 28 | 29 | # captcha integration for recover form 30 | # config.captcha_for_recover = true 31 | 32 | # captcha integration for sign up form 33 | # config.captcha_for_sign_up = true 34 | 35 | # captcha integration for sign in form 36 | # config.captcha_for_sign_in = true 37 | 38 | # captcha integration for unlock form 39 | # config.captcha_for_unlock = true 40 | 41 | # captcha integration for confirmation form 42 | # config.captcha_for_confirmation = true 43 | 44 | # Time period for account expiry from last_activity_at 45 | # config.expire_after = 90.days 46 | 47 | # Allow password to equal the email 48 | # config.allow_passwords_equal_to_email = false 49 | 50 | # paranoid_verification will regenerate verification code after failed attempt 51 | # config.paranoid_code_regenerate_after_attempt = 10 52 | end 53 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/friendly_id.rb: -------------------------------------------------------------------------------- 1 | # FriendlyId Global Configuration 2 | # 3 | # Use this to set up shared configuration options for your entire application. 4 | # Any of the configuration options shown here can also be applied to single 5 | # models by passing arguments to the `friendly_id` class method or defining 6 | # methods in your model. 7 | # 8 | # To learn more, check out the guide: 9 | # 10 | # http://norman.github.io/friendly_id/file.Guide.html 11 | 12 | FriendlyId.defaults do |config| 13 | # ## Reserved Words 14 | # 15 | # Some words could conflict with Rails's routes when used as slugs, or are 16 | # undesirable to allow as slugs. Edit this list as needed for your app. 17 | config.use :reserved 18 | 19 | config.reserved_words = %w(new edit index session login logout users admin stylesheets assets javascripts images) 20 | 21 | # ## Friendly Finders 22 | # 23 | # Uncomment this to use friendly finders in all models. By default, if 24 | # you wish to find a record by its friendly id, you must do: 25 | # 26 | # MyModel.friendly.find('foo') 27 | # 28 | # If you uncomment this, you can do: 29 | # 30 | # MyModel.find('foo') 31 | # 32 | # This is significantly more convenient but may not be appropriate for 33 | # all applications, so you must explicity opt-in to this behavior. You can 34 | # always also configure it on a per-model basis if you prefer. 35 | # 36 | # Something else to consider is that using the :finders addon boosts 37 | # performance because it will avoid Rails-internal code that makes runtime 38 | # calls to `Module.extend`. 39 | # 40 | # config.use :finders 41 | # 42 | # ## Slugs 43 | # 44 | # Most applications will use the :slugged module everywhere. If you wish 45 | # to do so, uncomment the following line. 46 | # 47 | # config.use :slugged 48 | # 49 | # By default, FriendlyId's :slugged addon expects the slug column to be named 50 | # 'slug', but you can change it if you wish. 51 | # 52 | # config.slug_column = 'slug' 53 | # 54 | # When FriendlyId can not generate a unique ID from your base method, it appends 55 | # a UUID, separated by a single dash. You can configure the character used as the 56 | # separator. If you're upgrading from FriendlyId 4, you may wish to replace this 57 | # with two dashes. 58 | # 59 | # config.sequence_separator = '-' 60 | # 61 | # ## Tips and Tricks 62 | # 63 | # ### Controlling when slugs are generated 64 | # 65 | # As of FriendlyId 5.0, new slugs are generated only when the slug field is 66 | # nil, but you if you're using a column as your base method can change this 67 | # behavior by overriding the `should_generate_new_friendly_id` method that 68 | # FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave 69 | # more like 4.0. 70 | # 71 | # config.use Module.new { 72 | # def should_generate_new_friendly_id? 73 | # slug.blank? || _changed? 74 | # end 75 | # } 76 | # 77 | # FriendlyId uses Rails's `parameterize` method to generate slugs, but for 78 | # languages that don't use the Roman alphabet, that's not usually suffient. Here 79 | # we use the Babosa library to transliterate Russian Cyrillic slugs to ASCII. If 80 | # you use this, don't forget to add "babosa" to your Gemfile. 81 | # 82 | # config.use Module.new { 83 | # def normalize_friendly_id(text) 84 | # text.to_slug.normalize! :transliterations => [:russian, :latin] 85 | # end 86 | # } 87 | end 88 | -------------------------------------------------------------------------------- /config/initializers/gaffe.rb: -------------------------------------------------------------------------------- 1 | Gaffe.configure do |config| 2 | config.errors_controller = { 3 | %r{^/killswitch} => 'API::ErrorsController', 4 | %r{^/} => 'Web::ErrorsController' 5 | } 6 | end 7 | 8 | Gaffe.enable! 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | inflect.acronym 'API' 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/initializers/mini_check.rb: -------------------------------------------------------------------------------- 1 | HealthChecks = MiniCheck::RackApp.new(path: '/health') 2 | HealthChecks.register('noop') { true } 3 | HealthChecks.register('database') { ActiveRecord::Base.connection.active? } 4 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults_5_2.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.2 upgrade. 4 | # 5 | # Once upgraded flip defaults one by one to migrate to the new default. 6 | # 7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 8 | 9 | # Make Active Record use stable #cache_key alongside new #cache_version method. 10 | # This is needed for recyclable cache keys. 11 | # Rails.application.config.active_record.cache_versioning = true 12 | 13 | # Use AES-256-GCM authenticated encryption for encrypted cookies. 14 | # Also, embed cookie expiry in signed or encrypted cookies for increased security. 15 | # 16 | # This option is not backwards compatible with earlier Rails versions. 17 | # It's best enabled when your entire app is migrated and stable on 5.2. 18 | # 19 | # Existing cookies will be converted on read then written with the new scheme. 20 | # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true 21 | 22 | # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages 23 | # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. 24 | # Rails.application.config.active_support.use_authenticated_message_encryption = true 25 | 26 | # Add default protection from forgery to ActionController::Base instead of in 27 | # ApplicationController. 28 | # Rails.application.config.action_controller.default_protect_from_forgery = true 29 | 30 | # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and 31 | # 'f' after migrating old data. 32 | # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true 33 | 34 | # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. 35 | # Rails.application.config.active_support.use_sha1_digests = true 36 | 37 | # Make `form_with` generate id attributes for any generated HTML tags. 38 | # Rails.application.config.action_view.form_with_generates_ids = true 39 | -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | if Rails.application.secrets.sentry_dsn.present? 2 | Sentry.init do |config| 3 | config.dsn = Rails.application.secrets.sentry_dsn 4 | config.breadcrumbs_logger = [:active_support_logger, :http_logger] 5 | 6 | config.excluded_exceptions += %w( 7 | ActionController::RoutingError 8 | ActiveRecord::RecordNotFound 9 | CanCan::AccessDenied 10 | ActionController::InvalidAuthenticityToken 11 | BehaviorDispatcher::MissingParameter 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Killswitch::Application.config.session_store :cookie_store, key: '_killswitch_session' 4 | -------------------------------------------------------------------------------- /config/initializers/sprockets_es6.rb: -------------------------------------------------------------------------------- 1 | Sprockets::ES6.configure do |config| 2 | config.moduleIds = true 3 | config.modules = 'amd' 4 | config.keepModuleIdExtensions = false 5 | config.loose = %w(es6.classes) 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/activerecord.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | activerecord: 4 | attributes: 5 | behavior: 6 | empty_time: N/A 7 | time_operators: 8 | lt: Before 9 | gt: After 10 | version_operators: 11 | lte: Less than or equals to 12 | lt: Less than 13 | eq: Equals to 14 | gt: Greater than 15 | gte: Greater than or equals to 16 | languages: 17 | any: Any language 18 | fr: French 19 | en: English 20 | de: German 21 | es: Spanish 22 | it: Italian 23 | pt: Portuguese 24 | membership: 25 | membership_types: 26 | admin: Administrator 27 | member: Member 28 | organization: 29 | memberships_count: Memberships 30 | applications_count: Applications 31 | errors: 32 | messages: 33 | invalid_email: is not valid email address 34 | invalid_version: is not valid version number 35 | -------------------------------------------------------------------------------- /config/locales/applications.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | applications: 5 | update: 6 | notice: The application was succesfully updated. 7 | create: 8 | notice: The application was succesfully created. 9 | destroy: 10 | notice: The application was succesfully removed. 11 | alert: There was a problem while removing the application. Please try again. 12 | edit: 13 | save: Save application 14 | cancel: Cancel 15 | edit_application: Edit %{application} 16 | new: 17 | create: Create application 18 | cancel: Cancel 19 | add_application: Add new application 20 | index: 21 | page_title: Applications 22 | add_application: Add new application 23 | add_first_project: Add first project 24 | empty_applications: There are currently no applications for this organization. 25 | show: 26 | edit: Edit application 27 | confirm_destroy: Are you sure you want to remove this application? 28 | page_title: '%{application}' 29 | -------------------------------------------------------------------------------- /config/locales/behaviors.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | behaviors: 5 | update: 6 | notice: The behavior was succesfully updated. 7 | create: 8 | notice: The behavior was succesfully created. 9 | destroy: 10 | notice: The behavior was succesfully removed. 11 | alert: There was a problem while removing the behavior. Please try again. 12 | edit: 13 | edit_behavior: Edit behavior for %{project} 14 | save: Save behavior 15 | cancel: Cancel 16 | new: 17 | add_behavior: Add behavior for %{project} 18 | create: Create behavior 19 | cancel: Cancel 20 | form: 21 | populate_with: Populate with 22 | ok: OK 23 | alert_message: Alert message 24 | kill_application: Kill application 25 | none: None 26 | any_language: Any language 27 | please_select: Please select 28 | mini: 29 | toggle: toggle 30 | confirm_destroy: Are you sure you want to remove this behavior? 31 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | devise: 4 | failure: 5 | already_authenticated: You are already signed in. 6 | inactive: Your account is not activated yet. 7 | invalid: Invalid email or password. 8 | invalid_token: Invalid authentication token. 9 | locked: Your account is locked. 10 | not_found_in_database: Invalid email or password. 11 | timeout: Your session expired. Please sign in again to continue. 12 | unauthenticated: You need to sign in or sign up before continuing. 13 | unconfirmed: You have to confirm your account before continuing. 14 | sessions: 15 | signed_in: Signed in successfully. 16 | signed_out: Signed out successfully. 17 | errors: 18 | messages: 19 | already_confirmed: was already confirmed, please try signing in 20 | confirmation_period_expired: needs to be confirmed within %{period}, please request a new one 21 | expired: has expired, please request a new one 22 | not_found: not found 23 | not_locked: was not locked 24 | not_saved: 25 | one: '1 error prohibited this %{resource} from being saved:' 26 | other: '%{count} errors prohibited this %{resource} from being saved:' 27 | -------------------------------------------------------------------------------- /config/locales/devise.security_extension.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | taken_in_past: 'was used previously.' 5 | equal_to_current_password: 'must be different than the current password.' 6 | equal_to_email: 'must be different than the email.' 7 | password_complexity: 8 | digit: 9 | one: must contain at least one digit 10 | other: must contain at least %{count} digits 11 | lower: 12 | one: must contain at least one lower-case letter 13 | other: must contain at least %{count} lower-case letters 14 | symbol: 15 | one: must contain at least one punctuation mark or symbol 16 | other: must contain at least %{count} punctuation marks or symbols 17 | upper: 18 | one: must contain at least one upper-case letter 19 | other: must contain at least %{count} upper-case letters 20 | devise: 21 | invalid_captcha: 'The captcha input was invalid.' 22 | invalid_security_question: 'The security question answer was invalid.' 23 | paranoid_verify: 24 | code_required: 'Please enter the code our support team provided' 25 | paranoid_verification_code: 26 | updated: Verification code accepted 27 | show: 28 | submit_verification_code: Submit verification code 29 | verification_code: Verification code 30 | submit: Submit 31 | password_expired: 32 | updated: 'Your new password is saved.' 33 | change_required: 'Your password is expired. Please renew your password.' 34 | show: 35 | renew_your_password: Renew your password 36 | current_password: Current password 37 | new_password: New password 38 | new_password_confirmation: Confirm new password 39 | change_my_password: Change my password 40 | failure: 41 | session_limited: 'Your login credentials were used in another browser. Please sign in again to continue in this browser.' 42 | expired: 'Your account has expired due to inactivity. Please contact the site administrator.' 43 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | -------------------------------------------------------------------------------- /config/locales/errors.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | errors: 5 | forbidden: 6 | content: You are not authorized to access this page. 7 | title: Forbidden 8 | internal_server_error: 9 | content: Unfortunately, an error occured and made it unable for us to render the page you requested. Please try again a little later. 10 | title: Internal server error 11 | not_found: 12 | content: The page you were looking for was not found. 13 | title: Page not found 14 | -------------------------------------------------------------------------------- /config/locales/home.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | home: 5 | show: 6 | intro: Killswitch is a clever control panel that allows mobile developers to apply runtime version-specific behaviors to their iOS or Android application. 7 | orphan: You are signed in but you do not belong to any organization. Please contact your system administrator. 8 | sign_in: Sign in 9 | -------------------------------------------------------------------------------- /config/locales/layouts.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | layouts: 4 | application: 5 | base_title: Killswitch 6 | app_title: Killswitch 7 | edit_account: Edit account 8 | logout: Logout 9 | applications: Applications 10 | users: Users 11 | manage_organizations: Manage organizations 12 | mailer: 13 | footer_html: Your friends @ Killswitch 14 | -------------------------------------------------------------------------------- /config/locales/memberships.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | memberships: 5 | update: 6 | notice: The user was succesfully updated. 7 | create: 8 | notice: The user was succesfully created. 9 | destroy: 10 | notice: The user was succesfully removed. 11 | alert: There was a problem while removing the user. Please try again. 12 | index: 13 | page_title: Users in %{organization} 14 | add_user: Add new user 15 | confirm_destroy: Are you sure you want to remove this user from this organization? 16 | empty_users: There are currently no users in this organization. 17 | edit: 18 | save: Save user 19 | edit_user: Edit %{user} 20 | cancel: Cancel 21 | new: 22 | create: Create user 23 | add_user: Add new user 24 | cancel: Cancel 25 | -------------------------------------------------------------------------------- /config/locales/organizations.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | organizations: 5 | index: 6 | page_title: Organizations 7 | add_organization: Add organization 8 | confirm_destroy: Are you sure you want to remove this organization? 9 | new: 10 | add_organization: Add organization 11 | create: Create 12 | cancel: Cancel 13 | edit: 14 | edit_organization: Edit %{organization} 15 | save: Save 16 | cancel: Cancel 17 | create: 18 | notice: The organization was succesfully created. 19 | update: 20 | notice: The organization was succesfully updated. 21 | destroy: 22 | notice: The organization was succesfully removed. 23 | alert: There was a problem while removing the organization. Please try again. 24 | -------------------------------------------------------------------------------- /config/locales/passwords.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | devise: 4 | passwords: 5 | new: 6 | intro: Enter your email and we’ll send instructions on how to change your password. 7 | send_instructions: Send instructions 8 | edit: 9 | intro: You’re almost there! Pick a new password before you continue. 10 | save: Save new password 11 | -------------------------------------------------------------------------------- /config/locales/projects.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | projects: 5 | update: 6 | notice: The project was succesfully updated. 7 | create: 8 | notice: The project was succesfully created. 9 | destroy: 10 | notice: The project was succesfully removed. 11 | alert: There was a problem while removing the project. Please try again. 12 | edit: 13 | edit_project: Edit %{project} 14 | save: Save project 15 | cancel: Cancel 16 | new: 17 | add_project: Add new project for %{application} 18 | create: Create project 19 | cancel: Cancel 20 | show: 21 | informations: Informations 22 | example: Example 23 | edit: Edit project 24 | confirm_destroy: Are you sure you want to remove this project? 25 | page_title: '%{project}' 26 | empty_behaviors: There are currently no behaviors for this project. 27 | -------------------------------------------------------------------------------- /config/locales/sessions.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | devise: 4 | sessions: 5 | new: 6 | sign_in: Sign In 7 | forgot_password: I forgot my password 8 | remember_me: Remember me 9 | -------------------------------------------------------------------------------- /config/locales/shared.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | shared: 3 | error_messages: 4 | message: 'The form could not be saved because of these errors:' 5 | exception_backtrace: 6 | backtrace: Backtrace 7 | parameters: Parameters 8 | -------------------------------------------------------------------------------- /config/locales/user_mailer.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | user_mailer: 4 | welcome_email: 5 | subject: Welcome to Killswitch 6 | -------------------------------------------------------------------------------- /config/locales/users.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | web: 4 | users: 5 | update: 6 | notice: The user was succesfully updated. 7 | edit: 8 | save: Save user 9 | edit_account: Edit %{user} 10 | cancel: Cancel 11 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Environment 2 | environment ENV.fetch('RACK_ENV') 3 | 4 | # Workers count 5 | workers ENV.fetch('PUMA_WORKERS', 1) 6 | 7 | # Threads count per worker 8 | min_threads = ENV.fetch('PUMA_MIN_THREADS', 0) 9 | max_threads = ENV.fetch('PUMA_MAX_THREADS', 5) 10 | threads min_threads, max_threads 11 | 12 | # Preload the app 13 | preload_app! 14 | 15 | # Run code when a worker is spawned 16 | on_worker_boot do 17 | # Set a global logger 18 | Rails.logger = ActiveSupport::Logger.new($stdout) 19 | 20 | # Set ActiveRecord config 21 | ActiveSupport.on_load(:active_record) do 22 | ActiveRecordConfigurationOverride.override! 23 | end 24 | 25 | # Set ActionController config 26 | ActiveSupport.on_load(:action_controller) do 27 | ActionController::Base.logger = Rails.logger 28 | end 29 | 30 | # Set ActionView config 31 | ActiveSupport.on_load(:action_view) do 32 | ActionView::Base.logger = Rails.logger 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Killswitch::Application.routes.draw do 2 | root to: 'web/home#show' 3 | devise_for :users, controllers: { sessions: 'web/sessions', passwords: 'web/passwords' } 4 | 5 | get '/ping', to: ->(_env) do 6 | response = { status: 'ok', version: Killswitch::Application::VERSION } 7 | [200, { 'Content-Type' => 'application/json' }, [response.to_json]] 8 | end 9 | 10 | namespace :web, path: '' do 11 | resources :users, only: [:edit, :update] 12 | 13 | resources :organizations, only: [:index, :new, :edit, :create, :update, :destroy] do 14 | resources :applications, only: [:index, :new, :edit, :create, :update, :destroy, :show] do 15 | resources :projects, only: [:new, :edit, :create, :update, :destroy, :show] do 16 | resources :behaviors, only: [:new, :edit, :create, :update, :destroy] do 17 | put :order, on: :collection, as: :order 18 | end 19 | end 20 | end 21 | 22 | resources :memberships 23 | end 24 | end 25 | 26 | namespace :api, path: 'killswitch' do 27 | get '', to: 'behaviors#show' 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /config/schemas/behavior_data.jsonschema: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "action": { 6 | "type": "string", 7 | "pattern": "^(ok|kill|alert)$" 8 | }, 9 | "message": { 10 | "type": "string" 11 | }, 12 | "buttons": { 13 | "type": "array", 14 | "items": { 15 | "type": "object", 16 | "properties": { 17 | "type": { 18 | "type": "string", 19 | "pattern": "^(url|cancel|reload)$" 20 | }, 21 | "label": { 22 | "type": "string" 23 | }, 24 | "url": { 25 | "type": "string" 26 | }, 27 | "order": { 28 | "type": "integer" 29 | } 30 | }, 31 | "required": [ 32 | "type", 33 | "label" 34 | ] 35 | } 36 | } 37 | }, 38 | "required": [ 39 | "action" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | <%= Rails.env %>: 2 | secret_key_base: <%= ENV['SECRET_KEY_BASE'] %> 3 | domain: <%= ENV['CANONICAL_HOST'] %> 4 | protocol: <%= ENV['CANONICAL_PROTOCOL'] || 'http' %> 5 | port: "<%= ENV['RACK_ENV'] == 'development' ? ENV['CANONICAL_PORT'] : nil %>" 6 | show_backtrace: <%= BooleanEnvironmentVariable.new(ENV['SHOW_BACKTRACE']).as_bool %> 7 | sentry_dsn: <%= ENV['SENTRY_DSN'] %> 8 | auth_username: <%= ENV['BASIC_AUTH_USERNAME'] %> 9 | auth_password: <%= ENV['BASIC_AUTH_PASSWORD'] %> 10 | mailer_from: <%= ENV['MAILER_FROM'] %> 11 | smtp_address: <%= ENV['SMTP_ADDRESS'] %> 12 | smtp_port: <%= ENV['SMTP_PORT'] %> 13 | smtp_username: <%= ENV['SMTP_USERNAME'] %> 14 | smtp_password: <%= ENV['SMTP_PASSWORD'] %> 15 | force_ssl: <%= BooleanEnvironmentVariable.new(ENV['FORCE_SSL']).as_bool %> 16 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | local: 2 | service: Disk 3 | root: <%= Rails.root.join("storage") %> 4 | 5 | test: 6 | service: Disk 7 | root: <%= Rails.root.join("tmp/storage") %> 8 | -------------------------------------------------------------------------------- /db/migrate/20131108183954_create_applications.rb: -------------------------------------------------------------------------------- 1 | class CreateApplications < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :applications do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20131108184040_create_projects.rb: -------------------------------------------------------------------------------- 1 | class CreateProjects < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :projects do |t| 4 | t.string :name 5 | t.references :app, index: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20131108184334_add_project_key.rb: -------------------------------------------------------------------------------- 1 | class AddProjectKey < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :projects, :key, :string 4 | add_index :projects, :key 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20131108191020_rename_project_app_id.rb: -------------------------------------------------------------------------------- 1 | class RenameProjectAppId < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :projects, :app_id, :application_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20131112125557_create_friendly_id_slugs.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendlyIdSlugs < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :friendly_id_slugs do |t| 4 | t.string :slug, :null => false 5 | t.integer :sluggable_id, :null => false 6 | t.string :sluggable_type, :limit => 50 7 | t.string :scope 8 | t.datetime :created_at 9 | end 10 | add_index :friendly_id_slugs, :sluggable_id 11 | add_index :friendly_id_slugs, [:slug, :sluggable_type] 12 | add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], :unique => true 13 | add_index :friendly_id_slugs, :sluggable_type 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20131112125627_add_slugs_to_applications_and_projects.rb: -------------------------------------------------------------------------------- 1 | class AddSlugsToApplicationsAndProjects < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :applications, :slug, :string 4 | add_column :projects, :slug, :string 5 | 6 | add_index :applications, :slug, unique: true 7 | add_index :projects, :slug 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20131112140005_create_behaviors.rb: -------------------------------------------------------------------------------- 1 | class CreateBehaviors < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :behaviors do |t| 4 | t.references :project 5 | t.string :version 6 | t.string :version_operator 7 | t.string :language 8 | t.json :data, default: '{}' 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20131112194936_add_sort_order_to_behaviors.rb: -------------------------------------------------------------------------------- 1 | class AddSortOrderToBehaviors < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :behaviors, :behavior_order, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20131119114353_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table(:users) do |t| 4 | t.string :name 5 | 6 | ## Database authenticatable 7 | t.string :email, null: false, default: "" 8 | t.string :encrypted_password, null: false, default: "" 9 | 10 | ## Rememberable 11 | t.datetime :remember_created_at 12 | 13 | ## Trackable 14 | t.integer :sign_in_count, default: 0, null: false 15 | t.datetime :current_sign_in_at 16 | t.datetime :last_sign_in_at 17 | t.string :current_sign_in_ip 18 | t.string :last_sign_in_ip 19 | 20 | t.timestamps 21 | end 22 | 23 | add_index :users, :email, unique: true 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /db/migrate/20131119155530_add_slug_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :users, :slug, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20131120155642_add_deleted_at_to_models.rb: -------------------------------------------------------------------------------- 1 | class AddDeletedAtToModels < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :applications, :deleted_at, :datetime 4 | add_index :applications, [:slug, :deleted_at], name: "index_applications_on_slug_and_deleted_at" 5 | 6 | add_column :projects, :deleted_at, :datetime 7 | add_index :projects, [:slug, :deleted_at], name: "index_projects_on_slug_and_deleted_at" 8 | 9 | add_column :behaviors, :deleted_at, :datetime 10 | add_index :behaviors, [:id, :deleted_at], name: "index_behaviors_on_id_and_deleted_at" 11 | 12 | add_column :users, :deleted_at, :datetime 13 | add_index :users, [:slug, :deleted_at], name: "index_users_on_slug_and_deleted_at" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20131224183944_remove_unique_index_on_applications.rb: -------------------------------------------------------------------------------- 1 | class RemoveUniqueIndexOnApplications < ActiveRecord::Migration[4.2] 2 | def up 3 | remove_index :applications, :slug 4 | end 5 | 6 | def down 7 | add_index :applications, :slug, unique: true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20131224184148_remove_a_lot_of_indexes.rb: -------------------------------------------------------------------------------- 1 | class RemoveALotOfIndexes < ActiveRecord::Migration[4.2] 2 | def up 3 | remove_index "applications", ["slug", "deleted_at"] 4 | remove_index "behaviors", ["id", "deleted_at"] 5 | remove_index "projects", ["application_id"] 6 | remove_index "projects", ["key"] 7 | remove_index "projects", ["slug", "deleted_at"] 8 | remove_index "projects", ["slug"] 9 | remove_index "users", ["email"] 10 | remove_index "users", ["slug", "deleted_at"] 11 | end 12 | 13 | def down 14 | add_index "applications", ["slug", "deleted_at"], name: "index_applications_on_slug_and_deleted_at", using: :btree 15 | add_index "behaviors", ["id", "deleted_at"], name: "index_behaviors_on_id_and_deleted_at", using: :btree 16 | add_index "projects", ["application_id"], name: "index_projects_on_application_id", using: :btree 17 | add_index "projects", ["key"], name: "index_projects_on_key", using: :btree 18 | add_index "projects", ["slug", "deleted_at"], name: "index_projects_on_slug_and_deleted_at", using: :btree 19 | add_index "projects", ["slug"], name: "index_projects_on_slug", using: :btree 20 | add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree 21 | add_index "users", ["slug", "deleted_at"], name: "index_users_on_slug_and_deleted_at", using: :btree 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20131224184526_create_organizations.rb: -------------------------------------------------------------------------------- 1 | class CreateOrganizations < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :organizations do |t| 4 | t.string :name 5 | t.string :slug 6 | 7 | t.timestamps 8 | end 9 | 10 | Organization.create(name: 'Mirego') 11 | 12 | add_column :applications, :organization_id, :integer 13 | 14 | Application.all.each do |application| 15 | application.update organization: Organization.first 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20131230183006_add_camaraderie.rb: -------------------------------------------------------------------------------- 1 | class AddCamaraderie < ActiveRecord::Migration[4.2] 2 | def up 3 | create_table :memberships do |t| 4 | t.references :user 5 | t.references :organization 6 | t.string :membership_type 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index "memberships", ["organization_id", "membership_type"], name: "index_memberships_on_organization_id_and_membership_type" 12 | add_index "memberships", ["organization_id", "user_id", "membership_type"], name: "index_memberships_on_everything", unique: true 13 | add_index "memberships", ["organization_id", "user_id"], name: "index_memberships_on_organization_id_and_user_id" 14 | add_index "memberships", ["organization_id"], name: "index_memberships_on_organization_id" 15 | add_index "memberships", ["user_id"], name: "index_memberships_on_user_id" 16 | end 17 | 18 | def down 19 | drop_table :memberships 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20131231152100_add_recoverable_fields_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRecoverableFieldsToUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | change_table :users do |t| 4 | t.string :reset_password_token 5 | t.datetime :reset_password_sent_at 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20140219181729_create_versions.rb: -------------------------------------------------------------------------------- 1 | class CreateVersions < ActiveRecord::Migration[4.2] 2 | def self.up 3 | create_table :versions do |t| 4 | t.string :item_type, :null => false 5 | t.integer :item_id, :null => false 6 | t.string :event, :null => false 7 | t.string :whodunnit 8 | t.text :object 9 | t.datetime :created_at 10 | end 11 | add_index :versions, [:item_type, :item_id] 12 | end 13 | 14 | def self.down 15 | remove_index :versions, [:item_type, :item_id] 16 | drop_table :versions 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20140220160947_rename_behavior_version_to_version_number.rb: -------------------------------------------------------------------------------- 1 | class RenameBehaviorVersionToVersionNumber < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :behaviors, :version, :version_number 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20141223195846_add_is_super_admin_to_organizations.rb: -------------------------------------------------------------------------------- 1 | class AddIsSuperAdminToOrganizations < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :organizations, :super_admin, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150610135602_add_organization_cache_counters.rb: -------------------------------------------------------------------------------- 1 | class AddOrganizationCacheCounters < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :organizations, :memberships_count, :integer, default: 0 4 | add_column :organizations, :applications_count, :integer, default: 0 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180420163300_add_datetime_to_behaviors.rb: -------------------------------------------------------------------------------- 1 | class AddDatetimeToBehaviors < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :behaviors, :time_operator, :string 4 | add_column :behaviors, :time, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250314190743_add_old_passwords_table.rb: -------------------------------------------------------------------------------- 1 | class AddOldPasswordsTable < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :old_passwords do |t| 4 | t.string :encrypted_password, null: false 5 | t.string :password_archivable_type, null: false 6 | t.integer :password_archivable_id, null: false 7 | t.datetime :created_at 8 | end 9 | 10 | add_index :old_passwords, [:password_archivable_type, :password_archivable_id], name: 'index_password_archivable' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20250403130915_add_unique_session_id_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueSessionIdToUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :users, :unique_session_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2025_04_03_130915) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "applications", id: :serial, force: :cascade do |t| 19 | t.string "name" 20 | t.datetime "created_at" 21 | t.datetime "updated_at" 22 | t.string "slug" 23 | t.datetime "deleted_at" 24 | t.integer "organization_id" 25 | end 26 | 27 | create_table "behaviors", id: :serial, force: :cascade do |t| 28 | t.integer "project_id" 29 | t.string "version_number" 30 | t.string "version_operator" 31 | t.string "language" 32 | t.json "data", default: {} 33 | t.datetime "created_at" 34 | t.datetime "updated_at" 35 | t.integer "behavior_order" 36 | t.datetime "deleted_at" 37 | t.string "time_operator" 38 | t.datetime "time" 39 | end 40 | 41 | create_table "friendly_id_slugs", id: :serial, force: :cascade do |t| 42 | t.string "slug", null: false 43 | t.integer "sluggable_id", null: false 44 | t.string "sluggable_type", limit: 50 45 | t.string "scope" 46 | t.datetime "created_at" 47 | t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true 48 | t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type" 49 | t.index ["sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_id" 50 | t.index ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type" 51 | end 52 | 53 | create_table "memberships", id: :serial, force: :cascade do |t| 54 | t.integer "user_id" 55 | t.integer "organization_id" 56 | t.string "membership_type" 57 | t.datetime "created_at" 58 | t.datetime "updated_at" 59 | t.index ["organization_id", "membership_type"], name: "index_memberships_on_organization_id_and_membership_type" 60 | t.index ["organization_id", "user_id", "membership_type"], name: "index_memberships_on_everything", unique: true 61 | t.index ["organization_id", "user_id"], name: "index_memberships_on_organization_id_and_user_id" 62 | t.index ["organization_id"], name: "index_memberships_on_organization_id" 63 | t.index ["user_id"], name: "index_memberships_on_user_id" 64 | end 65 | 66 | create_table "old_passwords", force: :cascade do |t| 67 | t.string "encrypted_password", null: false 68 | t.string "password_archivable_type", null: false 69 | t.integer "password_archivable_id", null: false 70 | t.datetime "created_at" 71 | t.index ["password_archivable_type", "password_archivable_id"], name: "index_password_archivable" 72 | end 73 | 74 | create_table "organizations", id: :serial, force: :cascade do |t| 75 | t.string "name" 76 | t.string "slug" 77 | t.datetime "created_at" 78 | t.datetime "updated_at" 79 | t.boolean "super_admin", default: false 80 | t.integer "memberships_count", default: 0 81 | t.integer "applications_count", default: 0 82 | end 83 | 84 | create_table "projects", id: :serial, force: :cascade do |t| 85 | t.string "name" 86 | t.integer "application_id" 87 | t.datetime "created_at" 88 | t.datetime "updated_at" 89 | t.string "key" 90 | t.string "slug" 91 | t.datetime "deleted_at" 92 | end 93 | 94 | create_table "users", id: :serial, force: :cascade do |t| 95 | t.string "name" 96 | t.string "email", default: "", null: false 97 | t.string "encrypted_password", default: "", null: false 98 | t.datetime "remember_created_at" 99 | t.integer "sign_in_count", default: 0, null: false 100 | t.datetime "current_sign_in_at" 101 | t.datetime "last_sign_in_at" 102 | t.string "current_sign_in_ip" 103 | t.string "last_sign_in_ip" 104 | t.datetime "created_at" 105 | t.datetime "updated_at" 106 | t.string "slug" 107 | t.datetime "deleted_at" 108 | t.string "reset_password_token" 109 | t.datetime "reset_password_sent_at" 110 | t.string "unique_session_id" 111 | end 112 | 113 | create_table "versions", id: :serial, force: :cascade do |t| 114 | t.string "item_type", null: false 115 | t.integer "item_id", null: false 116 | t.string "event", null: false 117 | t.string "whodunnit" 118 | t.text "object" 119 | t.datetime "created_at" 120 | t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" 121 | end 122 | 123 | end 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "killswitch", 3 | "repository": "https://github.com/mirego/killswitch", 4 | "license": "BSD-3-Clause", 5 | "version": "0.0.0", 6 | "dependencies": { 7 | "almond": "0.2.9", 8 | "bootstrap": "3.4.1", 9 | "html5sortable": "0.13.1", 10 | "jquery": "3.5.0", 11 | "jquery-ujs": "1.2.2" 12 | }, 13 | "devDependencies": { 14 | "babel-eslint": "10.1.0", 15 | "eslint": "^7.32.0", 16 | "eslint-config-standard": "^16.0.3", 17 | "eslint-plugin-import": "^2.25.4", 18 | "eslint-plugin-mirego": "0.0.1", 19 | "eslint-plugin-node": "^11.1.0", 20 | "eslint-plugin-promise": "^5.2.0", 21 | "postcss": ">=8.4.31", 22 | "prettier": "2.5.1", 23 | "stylelint": "13.13.1", 24 | "stylelint-config-mirego": "2.0.0", 25 | "stylelint-order": "4.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | singleQuote: true, 4 | bracketSpacing: false, 5 | trailingComma: 'none' 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "Executing migrations…" 5 | bundle exec rake db:migrate 6 | 7 | echo "Executing the main web process…" 8 | exec "$@" 9 | -------------------------------------------------------------------------------- /spec/factories/applications.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :application do 3 | # Attributes 4 | name { FFaker::Company.name } 5 | 6 | # Associations 7 | association :organization 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/factories/behaviors.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :behavior do 3 | # Attributes 4 | version_number { "#{Random.rand(1..10)}.#{Random.rand(1..9)}.#{Random.rand(1..9)}" } 5 | version_operator { Behavior::VERSION_OPERATORS.keys.sample } 6 | language { nil } 7 | data { '{"action":"ok"}' } 8 | 9 | association :project 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/organizations.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :organization do 3 | # Attributes 4 | name { FFaker::Company.name } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/projects.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :project do 3 | # Attributes 4 | name { FFaker::Company.name } 5 | 6 | # Associations 7 | association :application 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | # Attributes 4 | name { FFaker::Name.name } 5 | email { FFaker::Internet.email } 6 | password { FFaker.bothify('?#?#?#?#?#?#?#?#?#}') } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/application_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Application do 4 | describe :Factories do 5 | subject { create(:application) } 6 | it { should be_valid } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/behavior_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Behavior do 4 | describe :Factories do 5 | subject { create(:behavior) } 6 | it { should be_valid } 7 | end 8 | 9 | describe :InstanceMethods do 10 | let(:behavior) { build(:behavior) } 11 | 12 | describe :language= do 13 | context 'when passed a non-blank value' do 14 | before { behavior.language = 'fr' } 15 | it { expect(behavior.language).to eq 'fr' } 16 | end 17 | 18 | context 'when passed a blank value' do 19 | before { behavior.language = nil } 20 | it { expect(behavior.language).to be_nil } 21 | end 22 | 23 | context 'when passed nil' do 24 | before { behavior.language = '' } 25 | it { expect(behavior.language).to be_nil } 26 | end 27 | end 28 | 29 | describe :parsed_version do 30 | context 'when real version is present and makes sense' do 31 | before { behavior.version_number = '3.0' } 32 | it { expect(behavior.parsed_version).to eq '3.0' } 33 | end 34 | end 35 | 36 | describe :version_operator_method do 37 | context 'when version_operator exists' do 38 | before { behavior.version_operator = 'lt' } 39 | it { expect(behavior.version_operator_method).to be_present } 40 | end 41 | 42 | context 'when version_operator doesn’t exist' do 43 | before { behavior.version_operator = '🐼' } 44 | it { expect(behavior.version_operator_method).to be_nil } 45 | end 46 | end 47 | 48 | describe :time_operator_method do 49 | context 'when time_operator exists' do 50 | before { behavior.time_operator = 'lt' } 51 | it { expect(behavior.time_operator_method).to be_present } 52 | end 53 | 54 | context 'when time_operator doesn’t exist' do 55 | before { behavior.time_operator = '🐼' } 56 | it { expect(behavior.time_operator_method).to be_nil } 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/models/organization_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Organization do 4 | describe :Factories do 5 | subject { create(:organization) } 6 | it { should be_valid } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Project do 4 | describe :Factories do 5 | subject { create(:project) } 6 | it { should be_valid } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | describe :Factories do 5 | subject { create(:user) } 6 | it { should be_valid } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/requests/api/behaviors_requests_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe API::BehaviorsController, type: :request do 4 | describe 'GET /killswitch' do 5 | subject { response } 6 | before { get '/killswitch', params: } 7 | 8 | context 'with missing “version” parameter' do 9 | let(:params) { {} } 10 | it { expect(response.status).to eq 400 } 11 | end 12 | 13 | context 'with invalid “version” parameter' do 14 | let(:params) { { version: '🐸' } } 15 | it { expect(response.status).to eq 400 } 16 | end 17 | 18 | context 'with missing “key” parameter' do 19 | let(:params) { { version: '1.0' } } 20 | it { expect(response.status).to eq 400 } 21 | end 22 | 23 | context 'with unknown “key” parameter' do 24 | let(:params) { { version: '1.0', key: 'foo' } } 25 | it { expect(response.status).to eq 404 } 26 | end 27 | 28 | context 'with valid parameters' do 29 | let(:project) { create(:project) } 30 | let(:params) { { version: '1.0', key: project.key } } 31 | 32 | it { expect(response.headers['Vary']).to eq 'Accept-Language, Origin' } 33 | it { expect(response.status).to eq 200 } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/services/behavior_dispatcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BehaviorDispatcher do 4 | describe :InstanceMethods do 5 | describe :dispatch! do 6 | let(:request) { ActionDispatch::TestRequest.new('action_dispatch.request.parameters' => params) } 7 | let(:dispatcher) { BehaviorDispatcher.new } 8 | 9 | context 'with all required and valid parameters' do 10 | let(:project) { create(:project) } 11 | let(:params) { { key: project.key, version: '5.0.0' } } 12 | 13 | it { expect { dispatcher.dispatch!(request) }.to_not raise_error } 14 | end 15 | 16 | context 'with invalid parameters' do 17 | context 'with missing key' do 18 | let(:params) { { version: '5.0.0' } } 19 | it { expect { dispatcher.dispatch!(request) }.to raise_error(BehaviorDispatcher::MissingParameter, 'Missing or invalid “key” parameter') } 20 | end 21 | 22 | context 'with unknown project key' do 23 | let(:params) { { key: 'FOO', version: '5.0.0' } } 24 | it { expect { dispatcher.dispatch!(request) }.to raise_error(ActiveRecord::RecordNotFound) } 25 | end 26 | 27 | context 'with missing version key' do 28 | let(:params) { { key: 'FOO' } } 29 | it { expect { dispatcher.dispatch!(request) }.to raise_error(BehaviorDispatcher::MissingParameter, 'Missing or invalid “version” parameter') } 30 | end 31 | 32 | context 'with invalid version key' do 33 | let(:params) { { kee: 'FOO', version: '😈' } } 34 | it { expect { dispatcher.dispatch!(request) }.to raise_error(BehaviorDispatcher::MissingParameter, 'Missing or invalid “version” parameter') } 35 | end 36 | end 37 | end 38 | 39 | describe :matching_behavior do 40 | let(:request) { ActionDispatch::TestRequest.new(env) } 41 | let(:env) do 42 | env = ActionDispatch::TestRequest::DEFAULT_ENV 43 | env['HTTP_ACCEPT_LANGUAGE'] = language 44 | env['action_dispatch.request.parameters'] = params 45 | env['rack-accept.request'] = Rack::Accept::Request.new(env) 46 | env 47 | end 48 | 49 | let(:params) { { key: project.key, version: '5.0.0' } } 50 | let(:dispatcher) { BehaviorDispatcher.new } 51 | let(:project) { create(:project) } 52 | let(:language) { nil } 53 | let(:time_now) { Time.zone.parse('2018-04-24 09:00:00 UTC') } 54 | 55 | before do 56 | allow(Time).to receive(:now).and_return(time_now) 57 | behavior1 58 | behavior2 59 | dispatcher.dispatch!(request) 60 | end 61 | 62 | context 'with language' do 63 | context 'defined by HTTP header' do 64 | context 'with a behavior matching language and version' do 65 | let(:language) { 'fr' } 66 | let(:behavior1) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'eq', language: 'en') } 67 | let(:behavior2) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'eq', language: 'fr') } 68 | 69 | it { expect(dispatcher.matching_behavior).to eq behavior2 } 70 | end 71 | 72 | context 'with a behavior matching language but not version' do 73 | let(:language) { 'fr' } 74 | let(:behavior1) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'eq', language: 'en') } 75 | let(:behavior2) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'eq', language: 'fr') } 76 | 77 | it { expect(dispatcher.matching_behavior).to eq Behavior::DefaultBehavior } 78 | end 79 | end 80 | 81 | context 'defined by query string parameter' do 82 | let(:language) { nil } 83 | let(:params) { { key: project.key, version: '5.0.0', http_accept_language: language } } 84 | 85 | context 'with a behavior matching language and version' do 86 | let(:language) { 'fr' } 87 | let(:behavior1) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'eq', language: 'en') } 88 | let(:behavior2) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'eq', language: 'fr') } 89 | 90 | it { expect(dispatcher.matching_behavior).to eq behavior2 } 91 | end 92 | 93 | context 'with a behavior matching language but not version' do 94 | let(:language) { 'fr' } 95 | let(:behavior1) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'eq', language: 'en') } 96 | let(:behavior2) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'eq', language: 'fr') } 97 | 98 | it { expect(dispatcher.matching_behavior).to eq Behavior::DefaultBehavior } 99 | end 100 | end 101 | end 102 | 103 | context 'with versions' do 104 | context 'with a behavior matching a < operator' do 105 | let(:behavior1) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'lt') } 106 | let(:behavior2) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'lt') } 107 | 108 | it { expect(dispatcher.matching_behavior).to eq behavior2 } 109 | end 110 | 111 | context 'with a behavior matching a <= operator' do 112 | let(:behavior1) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'lte') } 113 | let(:behavior2) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'lte') } 114 | 115 | it { expect(dispatcher.matching_behavior).to eq behavior1 } 116 | end 117 | 118 | context 'with a behavior matching a == operator' do 119 | let(:behavior1) { create(:behavior, project:, version_number: '4.0.0', version_operator: 'eq') } 120 | let(:behavior2) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'eq') } 121 | 122 | it { expect(dispatcher.matching_behavior).to eq behavior2 } 123 | end 124 | 125 | context 'with a behavior matching a >= operator' do 126 | let(:behavior1) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'gte') } 127 | let(:behavior2) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'gte') } 128 | 129 | it { expect(dispatcher.matching_behavior).to eq behavior2 } 130 | end 131 | 132 | context 'with a behavior matching a > operator' do 133 | let(:behavior1) { create(:behavior, project:, version_number: '4.5.0', version_operator: 'gt') } 134 | let(:behavior2) { create(:behavior, project:, version_number: '5.0.0', version_operator: 'gt') } 135 | 136 | it { expect(dispatcher.matching_behavior).to eq behavior1 } 137 | end 138 | 139 | context 'with no behavior matching' do 140 | let(:behavior1) { create(:behavior, project:, version_number: '1.0.0', version_operator: 'eq') } 141 | let(:behavior2) { create(:behavior, project:, version_number: '2.0.0', version_operator: 'eq') } 142 | 143 | it { expect(dispatcher.matching_behavior).to eq Behavior::DefaultBehavior } 144 | end 145 | end 146 | 147 | context 'with times' do 148 | context 'with a behavior matching a < operator' do 149 | let(:behavior1) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'lt', time: '2018-04-24 08:00:00', time_operator: 'lt') } 150 | let(:behavior2) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'lt', time: '2018-04-24 10:00:00', time_operator: 'lt') } 151 | 152 | it { expect(dispatcher.matching_behavior).to eq behavior2 } 153 | end 154 | 155 | context 'with a behavior matching a > operator' do 156 | let(:behavior1) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'lt', time: '2018-04-24 08:00:00', time_operator: 'gt') } 157 | let(:behavior2) { create(:behavior, project:, version_number: '6.0.0', version_operator: 'lt', time: '2018-04-24 10:00:00', time_operator: 'gt') } 158 | 159 | it { expect(dispatcher.matching_behavior).to eq behavior1 } 160 | end 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /spec/services/behavior_sorter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BehaviorSorter do 4 | describe :reorder! do 5 | let(:project) { create(:project) } 6 | let(:behaviors) { create_list(:behavior, 10, project:) } 7 | 8 | let(:sorter) { BehaviorSorter.new(project) } 9 | before { sorter.reorder! order.map(&:to_s) } 10 | 11 | context 'with reverse order' do 12 | let(:order) { behaviors.map(&:id).reverse } 13 | it { expect(project.behaviors.map(&:id)).to eq order } 14 | end 15 | 16 | context 'with random order' do 17 | let(:order) { behaviors.map(&:id).shuffle } 18 | it { expect(project.behaviors.map(&:id)).to eq order } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rspec/rails' 4 | 5 | # Requires supporting ruby files with custom matchers and macros, etc, 6 | # in spec/support/ and its subdirectories. 7 | Rails.root.glob('spec/support/**/*.rb').each { |f| require f } 8 | 9 | # Checks for pending migrations before tests are run. 10 | # If you are not using ActiveRecord, you can remove this line. 11 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) 12 | 13 | RSpec.configure do |config| 14 | # Disable `should` syntax 15 | config.expect_with :rspec do |c| 16 | c.syntax = :expect 17 | end 18 | 19 | # Run specs in random order to surface order dependencies 20 | config.order = 'random' 21 | 22 | # Inject Factory helper methods 23 | config.include FactoryBot::Syntax::Methods 24 | 25 | # Reload all factories before running the tests (in case they 26 | # were preloaded with Spring) 27 | config.before(:suite) { FactoryBot.reload } 28 | 29 | config.before(:suite) { DatabaseCleaner.strategy = :truncation } 30 | config.before(:each) { DatabaseCleaner.start } 31 | config.after(:each) { DatabaseCleaner.clean } 32 | end 33 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: 'stylelint-config-mirego' 4 | }; 5 | -------------------------------------------------------------------------------- /vendor/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/killswitch/d747029c477e80aecb94eea1fb60999ad753b83a/vendor/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /vendor/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/killswitch/d747029c477e80aecb94eea1fb60999ad753b83a/vendor/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /vendor/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/killswitch/d747029c477e80aecb94eea1fb60999ad753b83a/vendor/assets/fonts/fontawesome-webfont.woff --------------------------------------------------------------------------------