bullet_train 1.2.10 → 1.2.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/account/two_factors_controller.rb +21 -5
  3. data/app/controllers/turbo_devise_controller.rb +1 -1
  4. data/app/helpers/account/forms_helper.rb +3 -1
  5. data/app/javascript/controllers/clipboard_controller.js +1 -1
  6. data/app/javascript/controllers/connection_workflow_controller.js +7 -0
  7. data/app/javascript/controllers/desktop_menu_controller.js +26 -0
  8. data/app/javascript/controllers/index.js +4 -0
  9. data/app/javascript/index.js +2 -1
  10. data/app/javascript/support/turn.js +183 -0
  11. data/app/models/concerns/users/base.rb +1 -1
  12. data/app/views/account/invitations/_form.html.erb +2 -2
  13. data/app/views/account/memberships/_form.html.erb +2 -2
  14. data/app/views/account/memberships/_membership.html.erb +1 -1
  15. data/app/views/account/teams/_breadcrumbs.html.erb +12 -5
  16. data/app/views/account/teams/_team.html.erb +4 -4
  17. data/app/views/account/two_factors/verify.js.erb +1 -0
  18. data/app/views/devise/registrations/_two_factor.html.erb +29 -8
  19. data/app/views/devise/sessions/new.html.erb +4 -2
  20. data/app/views/layouts/docs.html.erb +7 -7
  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 +3 -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 +20 -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/version.rb +1 -1
  44. data/lib/bullet_train.rb +2 -1
  45. data/lib/colorizer.rb +1 -1
  46. data/lib/tasks/bullet_train_tasks.rake +29 -12
  47. metadata +20 -8
  48. data/app/views/account/invitations/_invitation.json.jbuilder +0 -7
  49. data/app/views/account/invitations/index.json.jbuilder +0 -1
  50. data/app/views/account/invitations/show.json.jbuilder +0 -1
  51. data/app/views/account/teams/_team.json.jbuilder +0 -9
  52. data/app/views/account/teams/index.json.jbuilder +0 -1
  53. 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: 7d28fd8d3f1e1ce07073cc4aadfd04758fc3a13aab3723577508be2055269211
4
+ data.tar.gz: ee5a557dd99c80d925c6f6dcb3c6c4978a71a27ac295b700bb27b854194166b0
5
5
  SHA512:
6
- metadata.gz: 23e1d1200c9139fd37e892cd6bd025a26a22960a9076567ca16b14e6ff24445dbaf2cfe16258078f670beda1d86a7300ed089f4b1fadc72e2d08ee0636747e5e
7
- data.tar.gz: b9ee30bfe129d2e9d7492599ead1fe82d0768853f4e39ba03621c12ec2990bd0e4c24359c1d31d171e22908be376628e6411c01c6e4b1376f5eb486d1c941da9
6
+ metadata.gz: d4bf3b8d872805ffbbc916056943eff7999acc6d614b214a4f62cf89633fe487854b659c2aacac24e1dcf39b7c6a6176e7a79999f7be6cb4f2e2574e515b6aa5
7
+ data.tar.gz: f7e25e0b2eb89f1af4772e22b4752d0a9d8025b042c78a9b468d8a6c00fea4c08eec8c5c9ac4e51cddd9d91517cb9db5a0bb593b2b81b3eb746ed61bf51b1bc2
@@ -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
@@ -7,7 +7,7 @@ class TurboDeviseController < ApplicationController
7
7
  if get?
8
8
  raise error
9
9
  elsif has_errors? && default_action
10
- render rendering_options.merge(formats: :html, status: :unprocessable_entity)
10
+ render error_rendering_options.merge(formats: :html, status: :unprocessable_entity)
11
11
  else
12
12
  redirect_to navigation_location
13
13
  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,9 +1,9 @@
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 %>
1
+ <li class="bg-white shadow overflow-hidden sm:rounded-md dark:bg-slate-400">
2
+ <%= link_to [:account, team], class: "group block hover:bg-slate-50 dark:hover:bg-slate-400 dark:text-slate-800" do %>
3
3
  <div class="px-4 py-4 flex items-center sm:pl-8 sm:pr-6">
4
4
  <div class="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
5
5
  <div>
6
- <div class="flex text-xl font-semibold text-blue uppercase group-hover:text-blue-dark tracking-widest dark:text-white">
6
+ <div class="flex text-xl font-semibold text-blue uppercase group-hover:text-blue-800 tracking-widest dark:text-white">
7
7
  <%= team.name %>
8
8
  </div>
9
9
  </div>
@@ -14,7 +14,7 @@
14
14
  </div>
15
15
  </div>
16
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">
17
+ <svg class="h-5 w-5 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
18
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
19
  </svg>
20
20
  </div>
@@ -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>
@@ -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
@@ -14,7 +14,9 @@ Rails.application.routes.draw do
14
14
  # TODO we need to either implement a dashboard or deprecate this.
15
15
  root to: "dashboard#index", as: "dashboard"
16
16
 
17
- resource :two_factor, only: [:create, :destroy]
17
+ resource :two_factor, only: [:create, :destroy] do
18
+ post :verify
19
+ end
18
20
 
19
21
  # user-level onboarding tasks.
20
22
  namespace :onboarding do