bullet_train 1.2.10 → 1.2.12
Sign up to get free protection for your applications and to get access to all the features.
- 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
|