bullet_train 1.2.10 → 1.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/account/two_factors_controller.rb +21 -5
  3. data/app/helpers/account/forms_helper.rb +3 -1
  4. data/app/javascript/controllers/clipboard_controller.js +1 -1
  5. data/app/javascript/controllers/connection_workflow_controller.js +7 -0
  6. data/app/javascript/controllers/desktop_menu_controller.js +26 -0
  7. data/app/javascript/controllers/index.js +4 -0
  8. data/app/javascript/index.js +2 -1
  9. data/app/javascript/support/turn.js +183 -0
  10. data/app/models/concerns/users/base.rb +1 -1
  11. data/app/views/account/invitations/_form.html.erb +2 -2
  12. data/app/views/account/memberships/_form.html.erb +2 -2
  13. data/app/views/account/memberships/_membership.html.erb +1 -1
  14. data/app/views/account/teams/_breadcrumbs.html.erb +12 -5
  15. data/app/views/account/teams/_team.html.erb +3 -23
  16. data/app/views/account/two_factors/verify.js.erb +1 -0
  17. data/app/views/devise/registrations/_two_factor.html.erb +29 -8
  18. data/app/views/devise/sessions/new.html.erb +4 -2
  19. data/app/views/layouts/docs.html.erb +7 -7
  20. data/app/views/showcase/engine/_stylesheets.html.erb +1 -0
  21. data/config/locales/en/teams.en.yml +2 -0
  22. data/config/locales/en/users.en.yml +8 -1
  23. data/config/routes.rb +5 -1
  24. data/docs/action-models.md +5 -5
  25. data/docs/api/versioning.md +0 -2
  26. data/docs/api.md +4 -4
  27. data/docs/application-options.md +1 -1
  28. data/docs/billing/usage.md +94 -16
  29. data/docs/field-partials.md +21 -20
  30. data/docs/font-awesome-pro.md +1 -1
  31. data/docs/getting-started.md +3 -3
  32. data/docs/i18n.md +3 -3
  33. data/docs/indirection.md +6 -4
  34. data/docs/namespacing.md +1 -1
  35. data/docs/onboarding.md +8 -8
  36. data/docs/overriding.md +1 -1
  37. data/docs/permissions.md +1 -1
  38. data/docs/seeds.md +1 -1
  39. data/docs/testing.md +2 -1
  40. data/docs/themes.md +18 -11
  41. data/docs/tunneling.md +2 -2
  42. data/docs/upgrades.md +2 -1
  43. data/lib/bullet_train/engine.rb +6 -0
  44. data/lib/bullet_train/version.rb +1 -1
  45. data/lib/bullet_train.rb +2 -1
  46. data/lib/colorizer.rb +1 -1
  47. data/lib/tasks/bullet_train_tasks.rake +29 -12
  48. metadata +35 -9
  49. data/app/controllers/turbo_devise_controller.rb +0 -19
  50. data/app/views/account/invitations/_invitation.json.jbuilder +0 -7
  51. data/app/views/account/invitations/index.json.jbuilder +0 -1
  52. data/app/views/account/invitations/show.json.jbuilder +0 -1
  53. data/app/views/account/teams/_team.json.jbuilder +0 -9
  54. data/app/views/account/teams/index.json.jbuilder +0 -1
  55. data/app/views/account/teams/show.json.jbuilder +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6a06a235e23a68bf1e4855fb344aadba158538aa2ab823118fbebc252059649
4
- data.tar.gz: 2ec8399302ea2e4539e9593b8cabc741e5ad2eb1508a2f29e7591ec614d05098
3
+ metadata.gz: 807a5e0fe30a22bafa54c16b49271477fbe82c8180116b3448162851c4181482
4
+ data.tar.gz: f4dcb5d397fec1110576b95d0ca7c67bc975d244522f4708127b0bdeed78cc9b
5
5
  SHA512:
6
- metadata.gz: 23e1d1200c9139fd37e892cd6bd025a26a22960a9076567ca16b14e6ff24445dbaf2cfe16258078f670beda1d86a7300ed089f4b1fadc72e2d08ee0636747e5e
7
- data.tar.gz: b9ee30bfe129d2e9d7492599ead1fe82d0768853f4e39ba03621c12ec2990bd0e4c24359c1d31d171e22908be376628e6411c01c6e4b1376f5eb486d1c941da9
6
+ metadata.gz: a090d8380e20ea5852b2783e151b555452cf00320f05f6998dfa09e41c7c5cfcc3d76da0b7b888358074856ff5c2183ff23f4804e224e56d5cb849bdb8b6c578
7
+ data.tar.gz: bf8045613b7f1314ff5e34a3cf7be5fa6d54c5e536c5815d24770c7cde6f44f24f73ac659db76d6cd475fe6fd38559d91bfa22ffe720989a02eb3643354dba6d
@@ -1,18 +1,34 @@
1
1
  class Account::TwoFactorsController < Account::ApplicationController
2
2
  before_action :authenticate_user!
3
3
 
4
+ def verify
5
+ @user = current_user
6
+
7
+ otp_code = params["user"]["otp_attempt"]
8
+ @verified = current_user.validate_and_consume_otp!(otp_code)
9
+
10
+ if @verified
11
+ current_user.update(otp_required_for_login: true)
12
+ else
13
+ current_user.update(
14
+ otp_required_for_login: false,
15
+ otp_secret: nil
16
+ )
17
+ end
18
+ end
19
+
4
20
  def create
5
21
  @backup_codes = current_user.generate_otp_backup_codes!
6
22
  @user = current_user
7
23
 
8
- current_user.update(
9
- otp_secret: User.generate_otp_secret,
10
- otp_required_for_login: true
11
- )
24
+ current_user.update(otp_secret: User.generate_otp_secret)
12
25
  end
13
26
 
14
27
  def destroy
15
28
  @user = current_user
16
- current_user.update(otp_required_for_login: false)
29
+ current_user.update(
30
+ otp_required_for_login: false,
31
+ otp_secret: nil
32
+ )
17
33
  end
18
34
  end
@@ -40,7 +40,9 @@ module Account::FormsHelper
40
40
  def options_for(form, method)
41
41
  # e.g. "scaffolding/completely_concrete/tangible_things.fields.text_area_value.options"
42
42
  path = [model_key(form), (current_fields_namespace || :fields), method, :options]
43
- t(path.compact.join("."))
43
+ options = t(path.compact.join("."))
44
+ return options unless options.is_a?(Hash)
45
+ options.stringify_keys
44
46
  end
45
47
 
46
48
  def legacy_label_for(form, method)
@@ -9,7 +9,7 @@ export default class extends Controller {
9
9
  document.execCommand('copy')
10
10
  this.buttonTarget.innerHTML = '<i id="copied" class="fas fa-check w-4 h-4 block text-green-600"></i>'
11
11
  setTimeout(function () {
12
- document.getElementById('copied').innerHTML = '<i class="far fa-copy w-4 h-4 block text-gray-600"></i>'
12
+ document.getElementById('copied').innerHTML = '<i class="far fa-copy w-4 h-4 block text-slate-600"></i>'
13
13
  }, 1500)
14
14
  }
15
15
  }
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ disableTeamButton(event) {
5
+ document.body.style.pointerEvents = "none";
6
+ }
7
+ }
@@ -0,0 +1,26 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = [ "menuItemHeader", "menuItemGroup", "menuItemLink" ];
5
+
6
+ showSubmenu() {
7
+ this.menuItemGroupTarget.classList.remove('invisible');
8
+ }
9
+
10
+ // TODO: Stimulus JS should be able to use `keydown.tab` and `keydown.tab+shift` as actions.
11
+ // https://stimulus.hotwired.dev/reference/actions#keyboardevent-filter
12
+ hideSubmenu(event) {
13
+ let hideMenu = false;
14
+
15
+ // If we're tabbing forward and go outside of the submenu, we hide the group.
16
+ // Else if we're tabbing backwards and go outside via the menu item header, we hide the group.
17
+ if(event.key == 'Tab' && !event.shiftKey) {
18
+ let lastIndex = this.menuItemLinkTargets.length - 1;
19
+ hideMenu = event.target == this.menuItemLinkTargets[lastIndex]
20
+ } else if (event.key == 'Tab' && event.shiftKey) {
21
+ hideMenu = event.target == this.menuItemHeaderTarget
22
+ }
23
+
24
+ if(hideMenu) { this.menuItemGroupTarget.classList.add('invisible'); }
25
+ }
26
+ }
@@ -3,19 +3,23 @@ import { identifierForContextKey } from "@hotwired/stimulus-webpack-helpers"
3
3
  import BulkActionFormController from './bulk_action_form_controller'
4
4
  import BulkActionsController from './bulk_actions_controller'
5
5
  import ClipboardController from './clipboard_controller'
6
+ import DesktopMenuController from './desktop_menu_controller'
6
7
  import FormController from './form_controller'
7
8
  import MobileMenuController from './mobile_menu_controller'
8
9
  import TextToggleController from './text_toggle_controller'
9
10
  import SelectAllController from './select_all_controller'
11
+ import ConnectionWorkflowController from './connection_workflow_controller'
10
12
 
11
13
  export const controllerDefinitions = [
12
14
  [BulkActionFormController, 'bulk_action_form_controller.js'],
13
15
  [BulkActionsController, 'bulk_actions_controller.js'],
14
16
  [ClipboardController, 'clipboard_controller.js'],
17
+ [DesktopMenuController, 'desktop_menu_controller.js'],
15
18
  [FormController, 'form_controller.js'],
16
19
  [MobileMenuController, 'mobile_menu_controller.js'],
17
20
  [TextToggleController, 'text_toggle_controller.js'],
18
21
  [SelectAllController, 'select_all_controller.js'],
22
+ [ConnectionWorkflowController, 'connection_workflow_controller.js'],
19
23
  ].map(function(d) {
20
24
  const key = d[1]
21
25
  const controller = d[0]
@@ -1,2 +1,3 @@
1
1
  export * from './controllers'
2
- import './electron'
2
+ import './electron'
3
+ import './support/turn'
@@ -0,0 +1,183 @@
1
+ // MIT License
2
+ //
3
+ // Copyright (c) 2021 Dom Christie
4
+ //
5
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ // of this software and associated documentation files (the "Software"), to deal
7
+ // in the Software without restriction, including without limitation the rights
8
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ // copies of the Software, and to permit persons to whom the Software is
10
+ // furnished to do so, subject to the following conditions:
11
+ //
12
+ // The above copyright notice and this permission notice shall be included in all
13
+ // copies or substantial portions of the Software.
14
+ //
15
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ // SOFTWARE.
22
+
23
+ class Turn {
24
+ constructor (action) {
25
+ this.action = action
26
+ this.beforeExitClasses = new Set()
27
+ this.exitClasses = new Set()
28
+ this.enterClasses = new Set()
29
+ }
30
+
31
+ exit () {
32
+ this.animateOut = animationsEnd('[data-turn-exit]')
33
+ this.addClasses('before-exit')
34
+ requestAnimationFrame(() => {
35
+ this.addClasses('exit')
36
+ this.removeClasses('before-exit')
37
+ })
38
+ }
39
+
40
+ async beforeEnter (event) {
41
+ if (this.action === 'restore') return
42
+
43
+ event.preventDefault()
44
+
45
+ if (this.isPreview) {
46
+ this.hasPreview = true
47
+ await this.animateOut
48
+ } else {
49
+ await this.animateOut
50
+ if (this.animateIn) await this.animateIn
51
+ }
52
+
53
+ event.detail.resume()
54
+ }
55
+
56
+ async enter () {
57
+ this.removeClasses('exit')
58
+
59
+ if (this.shouldAnimateEnter) {
60
+ this.animateIn = animationsEnd('[data-turn-enter]')
61
+ this.addClasses('enter')
62
+ }
63
+ }
64
+
65
+ async complete () {
66
+ await this.animateIn
67
+ this.removeClasses('enter')
68
+ }
69
+
70
+ abort () {
71
+ this.removeClasses('before-exit')
72
+ this.removeClasses('exit')
73
+ this.removeClasses('enter')
74
+ }
75
+
76
+ get shouldAnimateEnter () {
77
+ if (this.action === 'restore') return false
78
+ if (this.isPreview) return true
79
+ if (this.hasPreview) return false
80
+ return true
81
+ }
82
+
83
+ get isPreview () {
84
+ return document.documentElement.hasAttribute('data-turbo-preview')
85
+ }
86
+
87
+ addClasses (type) {
88
+ document.documentElement.classList.add(`turn-${type}`)
89
+
90
+ Array.from(document.querySelectorAll(`[data-turn-${type}]`)).forEach((element) => {
91
+ element.dataset[`turn${pascalCase(type)}`].split(/\s+/).forEach((klass) => {
92
+ if (klass) {
93
+ element.classList.add(klass)
94
+ this[`${camelCase(type)}Classes`].add(klass)
95
+ }
96
+ })
97
+ })
98
+ }
99
+
100
+ removeClasses (type) {
101
+ document.documentElement.classList.remove(`turn-${type}`)
102
+
103
+ Array.from(document.querySelectorAll(`[data-turn-${type}]`)).forEach((element) => {
104
+ this[`${camelCase(type)}Classes`].forEach((klass) => element.classList.remove(klass))
105
+ })
106
+ }
107
+ }
108
+
109
+ Turn.start = function () {
110
+ if (motionSafe()) {
111
+ for (var event in this.eventListeners) {
112
+ addEventListener(event, this.eventListeners[event])
113
+ }
114
+ }
115
+ }
116
+
117
+ Turn.stop = function () {
118
+ for (var event in this.eventListeners) {
119
+ removeEventListener(event, this.eventListeners[event])
120
+ }
121
+ delete this.currentTurn
122
+ }
123
+
124
+ Turn.eventListeners = {
125
+ 'turbo:visit': function (event) {
126
+ if (this.currentTurn) this.currentTurn.abort()
127
+ this.currentTurn = new this(event.detail.action)
128
+ this.currentTurn.exit()
129
+ }.bind(Turn),
130
+ 'turbo:before-render': function (event) {
131
+ this.currentTurn.beforeEnter(event)
132
+ }.bind(Turn),
133
+ 'turbo:render': function () {
134
+ this.currentTurn.enter()
135
+ }.bind(Turn),
136
+ 'turbo:load': function () {
137
+ if (this.currentTurn) this.currentTurn.complete()
138
+ }.bind(Turn),
139
+ 'popstate': function () {
140
+ if (this.currentTurn && this.currentTurn.action !== 'restore') {
141
+ this.currentTurn.abort()
142
+ }
143
+ }.bind(Turn)
144
+ }
145
+
146
+ function prefersReducedMotion () {
147
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
148
+ return !mediaQuery || mediaQuery.matches
149
+ }
150
+
151
+ function motionSafe () {
152
+ return !prefersReducedMotion()
153
+ }
154
+
155
+ function animationsEnd (selector) {
156
+ const elements = [...document.querySelectorAll(selector)]
157
+
158
+ return Promise.all(elements.map((element) => {
159
+ return new Promise((resolve) => {
160
+ function listener () {
161
+ element.removeEventListener('animationend', listener)
162
+ resolve()
163
+ }
164
+ element.addEventListener('animationend', listener)
165
+ })
166
+ }))
167
+ }
168
+
169
+ function pascalCase (string) {
170
+ return string.split(/[^\w]/).map(capitalize).join('')
171
+ }
172
+
173
+ function camelCase (string) {
174
+ return string.split(/[^\w]/).map(
175
+ (w, i) => i === 0 ? w.toLowerCase() : capitalize(w)
176
+ ).join('')
177
+ }
178
+
179
+ function capitalize (string) {
180
+ return string.replace(/^\w/, (c) => c.toUpperCase())
181
+ }
182
+
183
+ Turn.start()
@@ -3,7 +3,7 @@ module Users::Base
3
3
 
4
4
  included do
5
5
  if two_factor_authentication_enabled?
6
- devise :two_factor_authenticatable, :two_factor_backupable, otp_secret_encryption_key: ENV["TWO_FACTOR_ENCRYPTION_KEY"]
6
+ devise :two_factor_authenticatable, :two_factor_backupable
7
7
  else
8
8
  devise :database_authenticatable
9
9
  end
@@ -23,10 +23,10 @@
23
23
  <% Membership.assignable_roles.each do |role| %>
24
24
  <% if current_membership.can_manage_role?(role) %>
25
25
  <div class="flex items-top">
26
- <%= fields.check_box :role_ids, {multiple: true, class: "h-4 w-4 text-blue focus:ring-blue-dark border-gray-300 rounded mt-0.5"}, role.id, nil %>
26
+ <%= fields.check_box :role_ids, {multiple: true, class: "h-4 w-4 text-blue focus:ring-blue-dark border-slate-300 rounded mt-0.5"}, role.id, nil %>
27
27
  <label for="invitation_membership_attributes_role_ids_<%= role.id %>" class="ml-2 block select-none">
28
28
  <span><%= t('invitations.form.invite_as', role_key: t("memberships.fields.role_ids.options.#{role.key}.label")) %></span>
29
- <div class="mt-0.5 text-gray-400 font-light leading-normal">
29
+ <div class="mt-0.5 text-slate-400 font-light leading-normal">
30
30
  <%= t("memberships.fields.role_ids.options.#{role.key}.description") %>
31
31
  </div>
32
32
  </label>
@@ -24,10 +24,10 @@
24
24
  <% Membership.assignable_roles.each do |role| %>
25
25
  <% if role.manageable_by?(current_membership.roles) %>
26
26
  <div class="flex items-top">
27
- <%= form.check_box :role_ids, {multiple: true, class: "h-4 w-4 text-blue focus:ring-blue-dark border-gray-300 rounded mt-0.5"}, role.id, nil %>
27
+ <%= form.check_box :role_ids, {multiple: true, class: "h-4 w-4 text-blue focus:ring-blue-800 border-slate-300 rounded mt-0.5"}, role.id, nil %>
28
28
  <label for="membership_role_ids_<%= role.id %>" class="ml-2 block select-none">
29
29
  <%= t('.grant_privileges_of', role_key: t(".fields.role_ids.options.#{role.key}.label")) %>
30
- <div class="mt-0.5 text-gray-400 font-light leading-normal">
30
+ <div class="mt-0.5 text-slate-400 font-light leading-normal">
31
31
  <%= t(".fields.role_ids.options.#{role.key}.description") %>
32
32
  </div>
33
33
  </label>
@@ -8,7 +8,7 @@
8
8
  <div class="ml-3">
9
9
  <span class="group-hover:underline"><%= membership.label_string %></span>
10
10
  <% if membership.unclaimed? %>
11
- <span class="ml-1.5 px-2 inline-flex text-xs text-green-dark bg-green-light border border-green-dark rounded-md">
11
+ <span class="ml-1.5 px-2 inline-flex text-xs text-green-800 bg-green-light border border-green-800 rounded-md">
12
12
  Invited
13
13
  </span>
14
14
  <% end %>
@@ -1,11 +1,18 @@
1
1
  <% team ||= @team %>
2
- <% unless current_user.one_team? %>
2
+
3
+ <% if (action_name == 'show' && controller_name == 'teams') || current_user.one_team? %>
4
+ <%= render 'account/shared/breadcrumb', label: t('.dashboard'), url: [:account, team], first: true %>
5
+ <% end %>
6
+
7
+ <% unless current_user.one_team? %>
3
8
  <%= render 'account/shared/breadcrumb', label: t('.label'), url: [:account, :teams], first: true %>
4
9
  <% if team&.persisted? %>
5
10
  <%= render 'account/shared/breadcrumb', label: team.name, url: [:account, team] %>
6
11
  <% end %>
7
12
  <% end %>
8
- <%= render 'account/shared/breadcrumbs/actions', only_for: 'teams' %>
9
- <% if (action_name == 'show' && controller_name == 'teams') || current_user.one_team? %>
10
- <%= render 'account/shared/breadcrumb', label: 'Dashboard', url: [:account, team] %>
11
- <% end %>
13
+
14
+ <%if action_name == 'edit' %>
15
+ <%= render 'account/shared/breadcrumb', label: t('.team_settings') %>
16
+ <%else %>
17
+ <%= render 'account/shared/breadcrumbs/actions', only_for: 'teams' %>
18
+ <% end %>
@@ -1,23 +1,3 @@
1
- <li class="bg-white shadow overflow-hidden sm:rounded-md dark:bg-sealBlue-400">
2
- <%= link_to [:account, team], class: "group block hover:bg-gray-50 dark:hover:bg-sealBlue-400 dark:text-sealBlue-800" do %>
3
- <div class="px-4 py-4 flex items-center sm:pl-8 sm:pr-6">
4
- <div class="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
5
- <div>
6
- <div class="flex text-xl font-semibold text-blue uppercase group-hover:text-blue-dark tracking-widest dark:text-white">
7
- <%= team.name %>
8
- </div>
9
- </div>
10
- <div class="mt-4 flex-shrink-0 sm:mt-0">
11
- <div class="flex overflow-hidden">
12
- <%= render 'account/shared/memberships/photos', memberships: team.memberships.current_and_invited.first(10) %>
13
- </div>
14
- </div>
15
- </div>
16
- <div class="ml-5 flex-shrink-0">
17
- <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
18
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
19
- </svg>
20
- </div>
21
- </div>
22
- <% end %>
23
- </li>
1
+ <%= render 'account/shared/team', link_url: [:account, team], memberships: team.memberships.current_and_invited.first(10) do |p| %>
2
+ <% p.content_for :name, team.name %>
3
+ <% end %>
@@ -0,0 +1 @@
1
+ $("#two-factor").html("<%= j render partial: "devise/registrations/two_factor", locals: {verified: @verified}%>");
@@ -1,9 +1,9 @@
1
1
  <%= render 'account/shared/box', divider: @backup_codes do |p| %>
2
2
  <% p.content_for :title, t("users.edit.two_factor.header") %>
3
3
  <% p.content_for :description, t("users.edit.two_factor.description_#{@user.otp_required_for_login? ? 'enabled' : 'disabled'}") %>
4
- <% p.content_for :body do %>
5
- <% if current_user.otp_required_for_login? %>
6
- <% if @backup_codes %>
4
+ <% if current_user.otp_secret %>
5
+ <% if @backup_codes %>
6
+ <% p.content_for :body do %>
7
7
 
8
8
  <%= render 'account/shared/alert' do %>
9
9
  <%= t('users.edit.two_factor.warning').html_safe %>
@@ -29,14 +29,35 @@
29
29
  <% end %>
30
30
  </center>
31
31
 
32
+ <%= form_for current_user, url: verify_account_two_factor_path, method: :post, remote:true, html: {class: 'form'} do |form| %>
33
+ <div class="py-4">
34
+ <%= render 'shared/fields/text_field', form: form, method: :otp_attempt %>
35
+ </div>
36
+ <%= form.submit t('users.edit.two_factor.buttons.verify'), class: 'button' %>
37
+ <% end %>
32
38
  <% end %>
33
39
  <% end %>
34
40
  <% end %>
35
41
  <% p.content_for :actions do %>
36
- <% if current_user.otp_required_for_login? %>
37
- <%= link_to t('users.edit.two_factor.buttons.disable'), account_two_factor_path, method: :delete, remote: true, class: "button" %>
38
- <% else %>
39
- <%= link_to t('users.edit.two_factor.buttons.enable'), account_two_factor_path, method: :post, remote: true, class: "button" %>
40
- <% end %>
42
+ <div class="<%= 'hidden' if @backup_codes %> space-y">
43
+ <% if local_assigns.has_key? :verified %>
44
+ <% if verified %>
45
+ <%= render 'account/shared/alert', color: 'blue' do %>
46
+ <%= t('users.edit.two_factor.verification_success').html_safe %>
47
+ <% end %>
48
+ <% else %>
49
+ <%= render 'account/shared/alert' do %>
50
+ <%= t('users.edit.two_factor.verification_fail').html_safe %>
51
+ <% end %>
52
+ <% end %>
53
+ <% end %>
54
+
55
+ <% if current_user.otp_required_for_login? %>
56
+ <%= link_to t('users.edit.two_factor.buttons.disable'), account_two_factor_path, method: :delete, remote: true, class: "button" %>
57
+ <% else %>
58
+ <%= link_to t('users.edit.two_factor.buttons.enable'), account_two_factor_path, method: :post, remote: true, class: "button" %>
59
+ <% end %>
60
+ </div>
41
61
  <% end %>
42
62
  <% end %>
63
+
@@ -2,7 +2,8 @@
2
2
  <% p.content_for :title, t('devise.headers.sign_in') %>
3
3
  <% p.content_for :body do %>
4
4
  <% within_fields_namespace(:self) do %>
5
- <%= form_for resource, as: resource_name, url: two_factor_authentication_enabled? ? users_pre_otp_path : session_path(resource_name), remote: two_factor_authentication_enabled?, html: {class: 'form'}, authenticity_token: true, data: {turbo: !user_return_to_is_oauth} do |form| %>
5
+ <%# TODO: Turbo is set to `false` for now, but we may want to only bypass Turbo for JavaScript-based requests in the future. %>
6
+ <%= form_for resource, as: resource_name, url: two_factor_authentication_enabled? ? users_pre_otp_path : session_path(resource_name), remote: two_factor_authentication_enabled?, html: {class: 'form'}, authenticity_token: true, data: {turbo: false} do |form| %>
6
7
  <% with_field_settings form: form do %>
7
8
  <%= render 'account/shared/notices', form: form %>
8
9
  <%= render 'account/shared/forms/errors', form: form %>
@@ -43,8 +44,9 @@
43
44
  </div>
44
45
 
45
46
  <% if devise_mapping.rememberable? %>
47
+ <% # TODO This needs to be its own component. Can't have this kind of styling here. %>
46
48
  <div class="flex items-center">
47
- <%= form.check_box :remember_me, class: "h-4 w-4 text-blue focus:ring-blue-dark border-gray-300 rounded" %>
49
+ <%= form.check_box :remember_me, class: "h-4 w-4 text-blue focus:ring-blue-800 border-slate-300 rounded dark:bg-slate-800 dark:border-slate-900" %>
48
50
  <%= form.label :remember_me, class: "ml-2 block" %>
49
51
  </div>
50
52
  <% end %>
@@ -32,7 +32,7 @@
32
32
  <meta property="og:url" content="<%= request.base_url + request.path %>" />
33
33
  <meta property="og:description" content="<%= description.truncate(200) %>" />
34
34
  </head>
35
- <body class="bg-light-gradient text-gray-700 text-sm font-normal dark:bg-dark-gradient dark:text-darkPrimary-300">
35
+ <body class="bg-light-gradient text-slate-700 text-sm font-normal dark:bg-800-gradient dark:text-slate-300">
36
36
  <div class="md:p-5">
37
37
  <div class="h-screen md:h-auto overflow-hidden md:rounded-lg flex shadow"
38
38
  data-controller="mobile-menu"
@@ -40,7 +40,7 @@
40
40
  >
41
41
 
42
42
  <% menu = capture do %>
43
- <div class="flex items-center flex-shrink-0 p-4 bg-blue-darker md:rounded-tl-lg">
43
+ <div class="flex items-center flex-shrink-0 p-4 bg-blue-700 md:rounded-tl-lg">
44
44
  <%= image_tag image_path("logo/logo.png"), class: 'h-5 w-auto mx-auto' %>
45
45
 
46
46
  <div class="lg:hidden absolute right-0">
@@ -147,7 +147,7 @@
147
147
  <% end %>
148
148
  <% end %>
149
149
 
150
- <%= render 'account/shared/menu/item', url: 'docs/application-options.md', label: 'Application Options' do |p| %>
150
+ <%= render 'account/shared/menu/item', url: '/docs/application-options', label: 'Application Options' do |p| %>
151
151
  <% p.content_for :icon do %>
152
152
  <i class="fal fa-gear ti ti-settings"></i>
153
153
  <% end %>
@@ -308,7 +308,7 @@
308
308
  data-transition-leave-start="translate-x-0"
309
309
  data-transition-leave-end="-translate-x-full"
310
310
 
311
- class="relative flex-1 flex flex-col max-w-xs w-full pb-4 bg-dark-gradient shadow-xl"
311
+ class="relative flex-1 flex flex-col max-w-xs w-full pb-4 bg-800-gradient shadow-xl"
312
312
  >
313
313
  <%= menu %>
314
314
  </div>
@@ -316,16 +316,16 @@
316
316
  </div>
317
317
  </div>
318
318
 
319
- <div class="hidden lg:flex lg:flex-shrink-0 overflow-y-auto bg-gradient-to-b from-primary-700 to-primary-800 dark:from-darkPrimary-800 dark:to-darkPrimary-800">
319
+ <div class="hidden lg:flex lg:flex-shrink-0 overflow-y-auto bg-gradient-to-b from-primary-700 to-primary-800 dark:from-slate-800 dark:to-slate-800">
320
320
  <div class="w-64">
321
321
  <%= menu %>
322
322
  </div>
323
323
  </div>
324
324
 
325
- <div class="flex flex-col w-0 flex-1 overflow-y-auto bg-gray-100 dark:bg-darkPrimary-800 lg:border-l dark:border-gray-500">
325
+ <div class="flex flex-col w-0 flex-1 overflow-y-auto bg-slate-100 dark:bg-slate-800 lg:border-l dark:border-slate-500">
326
326
  <main class="flex-1 relative z-0 overflow-y-auto focus:outline-none" tabindex="0">
327
327
 
328
- <button class="lg:hidden h-12 w-12 ml-1 flex-none inline-flex items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue"
328
+ <button class="lg:hidden h-12 w-12 ml-1 flex-none inline-flex items-center justify-center rounded-md text-slate-500 hover:text-slate-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue"
329
329
  data-action="mobile-menu#open"
330
330
  >
331
331
  <span class="sr-only">Open Application Menu</span>
@@ -0,0 +1 @@
1
+ <%= stylesheet_link_tag "application", "application.light", "showcase", "showcase.highlights" %>
@@ -4,6 +4,8 @@ en:
4
4
  singular: Team
5
5
  breadcrumbs:
6
6
  label: *label
7
+ team_settings: Team Settings
8
+ dashboard: Dashboard
7
9
  navigation:
8
10
  label: Team
9
11
  buttons: &buttons
@@ -17,12 +17,15 @@ en:
17
17
  header: Two-Factor Authentication
18
18
  description_enabled: 2FA is currently enabled for your account.
19
19
  description_disabled: You can increase the security of your account by enabling two-factor authentication.
20
- warning: "In order to complete set up, you <strong>must</strong> complete the steps below. <strong>The information below will not be shown again.</strong> If you don't complete setting up now, you must disable two-factor authentication to avoid being locked out of your account."
20
+ verification_fail: Verification failed. You can try enabling again. Please make sure you scan the new QR code with your Authenticator app.
21
+ verification_success: Success! Thanks for verifying. 2FA is now enabled.
22
+ warning: "In order to enable two-factor authentication, you <strong>must</strong> install an Authenticator app and <strong>enter your Verification Code from the app below</strong>. 2FA will not be enabled until you do so. This is to avoid you getting locked out of your account."
21
23
  instructions: "Install <a href='https://authy.com/download/' target='_blank'>Authy</a> or <a href='https://support.google.com/accounts/answer/1066447'>Google Authentication</a> on your phone and scan the barcode displayed below."
22
24
  recovery_codes: "You can also make a copy of the following recovery codes. Each one can help you get back into your account once should you lose access to the device you're using for two-factor authentication."
23
25
  buttons:
24
26
  enable: Enable
25
27
  disable: Disable
28
+ verify: Verify
26
29
  buttons: *buttons
27
30
  notifications:
28
31
  updated: User was successfully updated.
@@ -61,6 +64,10 @@ en:
61
64
  password_confirmation:
62
65
  _: &password_confirmation Password Confirmation
63
66
  label: Confirm Password
67
+ otp_attempt:
68
+ _: &otp_attempt Your Verification Code
69
+ label: *otp_attempt
70
+ heading: *otp_attempt
64
71
  locale:
65
72
  _: &locale Language
66
73
  label: *locale
data/config/routes.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  Rails.application.routes.draw do
2
+ mount Showcase::Engine, at: "/docs/showcase" if defined?(Showcase::Engine)
3
+
2
4
  scope module: "public" do
3
5
  root to: "home#index"
4
6
  get "invitation" => "home#invitation", :as => "invitation"
@@ -14,7 +16,9 @@ Rails.application.routes.draw do
14
16
  # TODO we need to either implement a dashboard or deprecate this.
15
17
  root to: "dashboard#index", as: "dashboard"
16
18
 
17
- resource :two_factor, only: [:create, :destroy]
19
+ resource :two_factor, only: [:create, :destroy] do
20
+ post :verify
21
+ end
18
22
 
19
23
  # user-level onboarding tasks.
20
24
  namespace :onboarding do