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.
- checksums.yaml +4 -4
- data/app/controllers/account/two_factors_controller.rb +21 -5
- data/app/helpers/account/forms_helper.rb +3 -1
- data/app/javascript/controllers/clipboard_controller.js +1 -1
- data/app/javascript/controllers/connection_workflow_controller.js +7 -0
- data/app/javascript/controllers/desktop_menu_controller.js +26 -0
- data/app/javascript/controllers/index.js +4 -0
- data/app/javascript/index.js +2 -1
- data/app/javascript/support/turn.js +183 -0
- data/app/models/concerns/users/base.rb +1 -1
- data/app/views/account/invitations/_form.html.erb +2 -2
- data/app/views/account/memberships/_form.html.erb +2 -2
- data/app/views/account/memberships/_membership.html.erb +1 -1
- data/app/views/account/teams/_breadcrumbs.html.erb +12 -5
- data/app/views/account/teams/_team.html.erb +3 -23
- data/app/views/account/two_factors/verify.js.erb +1 -0
- data/app/views/devise/registrations/_two_factor.html.erb +29 -8
- data/app/views/devise/sessions/new.html.erb +4 -2
- data/app/views/layouts/docs.html.erb +7 -7
- data/app/views/showcase/engine/_stylesheets.html.erb +1 -0
- data/config/locales/en/teams.en.yml +2 -0
- data/config/locales/en/users.en.yml +8 -1
- data/config/routes.rb +5 -1
- data/docs/action-models.md +5 -5
- data/docs/api/versioning.md +0 -2
- data/docs/api.md +4 -4
- data/docs/application-options.md +1 -1
- data/docs/billing/usage.md +94 -16
- data/docs/field-partials.md +21 -20
- data/docs/font-awesome-pro.md +1 -1
- data/docs/getting-started.md +3 -3
- data/docs/i18n.md +3 -3
- data/docs/indirection.md +6 -4
- data/docs/namespacing.md +1 -1
- data/docs/onboarding.md +8 -8
- data/docs/overriding.md +1 -1
- data/docs/permissions.md +1 -1
- data/docs/seeds.md +1 -1
- data/docs/testing.md +2 -1
- data/docs/themes.md +18 -11
- data/docs/tunneling.md +2 -2
- data/docs/upgrades.md +2 -1
- data/lib/bullet_train/engine.rb +6 -0
- data/lib/bullet_train/version.rb +1 -1
- data/lib/bullet_train.rb +2 -1
- data/lib/colorizer.rb +1 -1
- data/lib/tasks/bullet_train_tasks.rake +29 -12
- metadata +35 -9
- data/app/controllers/turbo_devise_controller.rb +0 -19
- data/app/views/account/invitations/_invitation.json.jbuilder +0 -7
- data/app/views/account/invitations/index.json.jbuilder +0 -1
- data/app/views/account/invitations/show.json.jbuilder +0 -1
- data/app/views/account/teams/_team.json.jbuilder +0 -9
- data/app/views/account/teams/index.json.jbuilder +0 -1
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 807a5e0fe30a22bafa54c16b49271477fbe82c8180116b3448162851c4181482
|
4
|
+
data.tar.gz: f4dcb5d397fec1110576b95d0ca7c67bc975d244522f4708127b0bdeed78cc9b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
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-
|
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,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]
|
data/app/javascript/index.js
CHANGED
@@ -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
|
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-
|
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-
|
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-
|
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-
|
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-
|
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
|
-
|
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
|
-
|
9
|
-
<%
|
10
|
-
<%= render 'account/shared/breadcrumb', label: '
|
11
|
-
<%
|
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
|
-
|
2
|
-
|
3
|
-
|
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
|
-
<%
|
5
|
-
<% if
|
6
|
-
<%
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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-
|
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-
|
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-
|
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
|
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-
|
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-
|
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-
|
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-
|
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" %>
|
@@ -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
|
-
|
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
|