├── .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 |
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 |
15 |
16 |
29 |
30 | <% if current_user.present? %>
31 | <% if current_organization.present? %>
32 |
33 |
34 | <%= present(current_organization).name %>
35 |
51 |
52 | ><%= link_to t('.applications'), web_organization_applications_path %>
53 | ><%= link_to t('.users'), web_organization_memberships_path %>
54 |
55 | <% end %>
56 |
57 |
67 | <% end %>
68 |
69 |
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 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | <%= link_to image_tag('img-favicon.png', width: 65, border: 0), root_url %>
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | <%= yield %>
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | <%= t('.footer_html', url: root_url, style: inline_styles(:'color' => '#888888', :'font-size' => '11px')) %>
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/app/views/shared/_error_messages.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
<%= t('.message') %>
3 |
4 | <% record.errors.full_messages.each do |message| %>
5 | <%= message %>
6 | <% end %>
7 |
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 |
6 | <% present @applications do |application| %>
7 |
8 | <%= link_to application.name, web_organization_application_path(id: application) %>
9 | <% if application.projects.any? %>
10 |
11 | <% application.projects.each do |project| %>
12 | <%= link_to project.name, web_organization_application_project_path(application_id: application, id: project) %>
13 | <% end %>
14 |
15 | <% else %>
16 | <%= link_to icon(:plus, t('.add_first_project')), new_web_organization_application_project_path(application_id: application), class: 'btn btn-xs' %>
17 | <% end %>
18 |
19 | <% end %>
20 |
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 |
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 |
14 |
15 |
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 | <%= Membership.human_attribute_name(:id) %>
10 | <%= Membership.human_attribute_name(:name) %>
11 | <%= Membership.human_attribute_name(:email) %>
12 | <%= Membership.human_attribute_name(:membership_type) %>
13 |
14 |
15 | <% present @memberships do |membership| %>
16 |
17 | <%= membership.user.id %>
18 | <% present membership.user do |user| %>
19 | <%= user.name %>
20 | <%= user.email %>
21 | <% end %>
22 | <%= membership.human_membership_type %>
23 |
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 |
29 |
30 | <% end %>
31 |
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 | <%= Organization.human_attribute_name(:id) %>
8 | <%= Organization.human_attribute_name(:name) %>
9 | <%= Organization.human_attribute_name(:applications_count) %>
10 | <%= Organization.human_attribute_name(:memberships_count) %>
11 | <%= Organization.human_attribute_name(:admin) %>
12 |
13 |
14 | <% present @organizations do |organization| %>
15 |
16 | <%= organization.id %>
17 | <%= organization.name %>
18 | <%= link_to organization.applications_count, web_organization_applications_path(organization) %>
19 | <%= link_to organization.memberships_count, web_organization_memberships_path(organization) %>
20 | <%= bool_icon(organization.super_admin?) %>
21 |
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 |
27 |
28 | <% end %>
29 |
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 | <%= Project.human_attribute_name(:key) %>
11 | <%= project.key %>
12 |
13 |
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 |
22 | <% present project.behaviors do |behavior| %>
23 | <%= render partial: 'web/behaviors/mini', locals: { behavior: behavior } %>
24 | <% end %>
25 |
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
--------------------------------------------------------------------------------