lesli 5.0.5 → 5.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/icons/lesli/cloud-letter.svg +1 -0
- data/app/assets/icons/readme.md +1 -1
- data/app/assets/javascripts/lesli/users/passwords.js +84 -118
- data/app/assets/javascripts/lesli/users/registrations.js +85 -119
- data/app/assets/javascripts/lesli/users/sessions.js +76 -130
- data/app/controllers/lesli/system_controllers_controller.rb +63 -0
- data/app/controllers/users/sessions_controller.rb +2 -1
- data/app/helpers/lesli/navigation_helper.rb +9 -9
- data/app/lib/lesli/system.rb +2 -1
- data/app/models/concerns/account_initializer.rb +27 -9
- data/app/models/concerns/user_extensions.rb +8 -6
- data/app/models/concerns/user_security.rb +1 -1
- data/app/models/lesli/account.rb +3 -1
- data/app/models/lesli/descriptor/privilege.rb +1 -0
- data/app/models/lesli/descriptor.rb +1 -4
- data/app/models/lesli/role/power.rb +3 -3
- data/app/models/lesli/user/session.rb +4 -27
- data/app/models/lesli/user.rb +2 -3
- data/app/operators/lesli/descriptor_privilege_operator.rb +1 -1
- data/app/operators/lesli/{role_power_operator.rb → role_descriptor_operator.rb} +24 -7
- data/app/views/lesli/partials/_application-data.html.erb +7 -0
- data/app/views/lesli/partials/_application-lesli-engines.html.erb +2 -2
- data/app/views/lesli/partials/_application-lesli-icons.html.erb +1 -1
- data/config/initializers/lesli.rb +2 -0
- data/config/locales/translations.en.yml +9 -2
- data/config/locales/translations.es.yml +9 -2
- data/config/routes.rb +2 -0
- data/db/migrate/v1.0/0010005010_create_lesli_descriptors.rb +1 -1
- data/db/migrate/v1.0/0010005510_create_lesli_role_powers.rb +7 -0
- data/lib/lesli/version.rb +1 -1
- data/lib/sass/lesli/components/editor-richtext.scss +10 -0
- data/lib/sass/lesli/templates/application.scss +4 -0
- data/lib/{tasks/lesli/privileges.rake → sass/lesli/templates/component.scss} +6 -21
- data/lib/tasks/lesli/db.rake +2 -1
- data/lib/tasks/lesli_tasks.rake +8 -0
- data/lib/vue/application.js +64 -62
- data/lib/vue/devise/sessions.js +1 -1
- data/lib/vue/layouts/application-header.vue +6 -7
- data/lib/vue/public.js +2 -5
- data/lib/vue/shared/stores/systemController.js +67 -0
- data/lib/vue/stores/translations.json +24 -2
- data/lib/vue/stores/users.js +2 -6
- data/lib/webpack/base.js +3 -2
- data/lib/webpack/core.js +1 -1
- data/readme.md +1 -1
- metadata +22 -5
- /data/app/assets/icons/lesli/{cloud-help.svg → cloud-support.svg} +0 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
Lesli
|
4
|
+
|
5
|
+
Copyright (c) 2023, Lesli Technologies, S. A.
|
6
|
+
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
8
|
+
it under the terms of the GNU General Public License as published by
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
10
|
+
(at your option) any later version.
|
11
|
+
|
12
|
+
This program is distributed in the hope that it will be useful,
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
GNU General Public License for more details.
|
16
|
+
|
17
|
+
You should have received a copy of the GNU General Public License
|
18
|
+
along with this program. If not, see http://www.gnu.org/licenses/.
|
19
|
+
|
20
|
+
Lesli · Ruby on Rails SaaS Development Framework.
|
21
|
+
|
22
|
+
Made with ♥ by https://www.lesli.tech
|
23
|
+
Building a better future, one line of code at a time.
|
24
|
+
|
25
|
+
@contact hello@lesli.tech
|
26
|
+
@website https://www.lesli.tech
|
27
|
+
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
28
|
+
|
29
|
+
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
30
|
+
// ·
|
31
|
+
=end
|
32
|
+
|
33
|
+
module Lesli
|
34
|
+
class SystemControllersController < ApplicationLesliController
|
35
|
+
before_action :set_system_controller, only: [:show, :update, :destroy]
|
36
|
+
|
37
|
+
# GET /system_controllers
|
38
|
+
def index
|
39
|
+
respond_to do |format|
|
40
|
+
format.html {}
|
41
|
+
format.json do
|
42
|
+
respond_with_successful(Lesli::SystemController.index())
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def options
|
48
|
+
respond_to do |format|
|
49
|
+
format.html {}
|
50
|
+
format.json do
|
51
|
+
respond_with_successful(SystemController.options(current_user, @query))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Only allow a list of trusted parameters through.
|
59
|
+
def system_controller_params
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -109,7 +109,8 @@ class Users::SessionsController < Devise::SessionsController
|
|
109
109
|
|
110
110
|
# respond successful and send the path user should go
|
111
111
|
#respond_with_successful({ default_path: user.has_role_with_default_path?() })
|
112
|
-
respond_with_successful({ default_path: "/" })
|
112
|
+
respond_with_successful({ default_path: Lesli.config.path_after_login || "/" })
|
113
|
+
|
113
114
|
end
|
114
115
|
|
115
116
|
private
|
@@ -140,12 +140,12 @@ module Lesli
|
|
140
140
|
controller_path.include?("cloud_focus"))
|
141
141
|
end
|
142
142
|
|
143
|
-
# 03.05
|
144
|
-
def
|
145
|
-
return unless defined?
|
143
|
+
# 03.05 Letter engine
|
144
|
+
def navigation_engine_letter(title: "Letter", subtitle: "Notes & Notebooks")
|
145
|
+
return unless defined? LesliLetter
|
146
146
|
|
147
|
-
navigation_engine_item(title, subtitle, "
|
148
|
-
controller_path.include?("
|
147
|
+
navigation_engine_item(title, subtitle, "letter", lesli_letter.root_path,
|
148
|
+
controller_path.include?("lesli_letter"))
|
149
149
|
end
|
150
150
|
|
151
151
|
# 03.07 Social engine
|
@@ -244,11 +244,11 @@ module Lesli
|
|
244
244
|
end
|
245
245
|
|
246
246
|
# 07.02 Help engine
|
247
|
-
def
|
248
|
-
return unless defined?
|
247
|
+
def navigation_engine_support(title: "Support", subtitle: "Support Ticket System")
|
248
|
+
return unless defined? LesliSupport
|
249
249
|
|
250
|
-
navigation_engine_item(title, subtitle, "
|
251
|
-
controller_path.include?("
|
250
|
+
navigation_engine_item(title, subtitle, "support", lesli_support.root_path,
|
251
|
+
controller_path.include?("lesli_support"))
|
252
252
|
end
|
253
253
|
|
254
254
|
# 07.03 Portal engine
|
data/app/lib/lesli/system.rb
CHANGED
@@ -52,9 +52,9 @@ module AccountInitializer
|
|
52
52
|
limited = self.roles.create({ name: "limited", active: true, object_level_permission: 10, path_default: "/administration/profile" })
|
53
53
|
|
54
54
|
# assign descriptors with appropriate privileges
|
55
|
-
owner.powers.create(:descriptor => descriptor_owner)
|
56
|
-
sysadmin.powers.create(:descriptor => descriptor_sysadmin)
|
57
|
-
limited.powers.create(:descriptor => descriptor_profile)
|
55
|
+
owner.powers.create(:descriptor => descriptor_owner, :plist => true, :pindex => true, :pshow => true, :pcreate => true, :pcreate => true, :pupdate => true, :pdestroy => true)
|
56
|
+
sysadmin.powers.create(:descriptor => descriptor_sysadmin, :plist => true, :pindex => true, :pshow => true, :pcreate => true, :pcreate => true, :pupdate => true, :pdestroy => true)
|
57
|
+
limited.powers.create(:descriptor => descriptor_profile, :plist => true, :pindex => true, :pshow => true, :pcreate => true, :pcreate => true, :pupdate => true, :pdestroy => true)
|
58
58
|
end
|
59
59
|
|
60
60
|
|
@@ -79,6 +79,24 @@ module AccountInitializer
|
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
+
# 03.05 LesliNotes - Notes & Notebooks
|
83
|
+
if defined? LesliLetter
|
84
|
+
if self.letter.blank?
|
85
|
+
self.letter = LesliLetter::Account.new
|
86
|
+
self.letter.account = self
|
87
|
+
self.letter.save!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# 05.02 LesliAudit - System analytics
|
92
|
+
if defined? LesliAudit
|
93
|
+
if self.audit.blank?
|
94
|
+
self.audit = LesliAudit::Account.new
|
95
|
+
self.audit.account = self
|
96
|
+
self.audit.save!
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
82
100
|
# 07.02 LesliHelp - Support Ticket System
|
83
101
|
if defined? LesliHelp
|
84
102
|
if self.help.blank?
|
@@ -88,12 +106,12 @@ module AccountInitializer
|
|
88
106
|
end
|
89
107
|
end
|
90
108
|
|
91
|
-
# 08.
|
92
|
-
if defined?
|
93
|
-
if self.
|
94
|
-
self.
|
95
|
-
self.
|
96
|
-
self.
|
109
|
+
# 08.01 LesliGuard - Security Management Module
|
110
|
+
if defined? LesliGuard
|
111
|
+
if self.guard.blank?
|
112
|
+
self.guard = LesliGuard::Account.new
|
113
|
+
self.guard.account = self
|
114
|
+
self.guard.save!
|
97
115
|
end
|
98
116
|
end
|
99
117
|
end
|
@@ -101,14 +101,16 @@ module UserExtensions
|
|
101
101
|
# locale = User.last.locle
|
102
102
|
# will print something like: :es
|
103
103
|
def locale
|
104
|
-
user_locale = settings.find_by(name: "locale")
|
104
|
+
user_locale = self.settings.find_by(name: "locale")
|
105
105
|
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
# return the desire locale by the user
|
107
|
+
return user_locale.value.to_sym if user_locale
|
108
|
+
|
109
|
+
# create a desire locale if the record does not exist
|
110
|
+
self.settings.create_with(:value => I18n.locale).find_or_create_by(:name => "locale")
|
109
111
|
|
110
|
-
#
|
111
|
-
|
112
|
+
# reevaluate
|
113
|
+
self.locale()
|
112
114
|
end
|
113
115
|
|
114
116
|
|
@@ -111,7 +111,7 @@ module UserSecurity
|
|
111
111
|
def can_work_with_role?(role)
|
112
112
|
|
113
113
|
# get the role if only id is given
|
114
|
-
role = self.account.roles.find_by(:id => role) unless role.class.name == "Role"
|
114
|
+
role = self.account.roles.find_by(:id => role) unless role.class.name == "Lesli::Role"
|
115
115
|
|
116
116
|
# false if role not found
|
117
117
|
return false if role.blank?
|
data/app/models/lesli/account.rb
CHANGED
@@ -32,7 +32,6 @@ Building a better future, one line of code at a time.
|
|
32
32
|
|
33
33
|
module Lesli
|
34
34
|
class Account < ApplicationLesliRecord
|
35
|
-
|
36
35
|
include AccountInitializer
|
37
36
|
|
38
37
|
|
@@ -54,10 +53,13 @@ module Lesli
|
|
54
53
|
has_many :currencies
|
55
54
|
has_many :logs
|
56
55
|
|
56
|
+
|
57
57
|
has_one :help, class_name: "LesliHelp::Account"
|
58
58
|
has_one :audit, class_name: "LesliAudit::Account"
|
59
59
|
has_one :admin, class_name: "LesliAdmin::Account"
|
60
60
|
has_one :driver, class_name: "LesliDriver::Account"
|
61
|
+
has_one :letter, class_name: "LesliLetter::Account"
|
62
|
+
has_one :guard, class_name: "LesliGuard::Account"
|
61
63
|
|
62
64
|
|
63
65
|
# account statuses
|
@@ -34,5 +34,6 @@ module Lesli
|
|
34
34
|
class Descriptor::Privilege < ApplicationLesliRecord
|
35
35
|
belongs_to :descriptor
|
36
36
|
belongs_to :action, class_name: "SystemController::Action"
|
37
|
+
belongs_to :system_controller_action, class_name: "SystemController::Action", foreign_key: "action_id"
|
37
38
|
end
|
38
39
|
end
|
@@ -34,7 +34,6 @@ module Lesli
|
|
34
34
|
class Descriptor < ApplicationLesliRecord
|
35
35
|
belongs_to :account
|
36
36
|
has_many :privileges
|
37
|
-
#has_many :role_descriptors
|
38
37
|
|
39
38
|
# this scope is needed to allow to join with deleted descriptors
|
40
39
|
# join with deleted descriptors is needed to know which privileges we have to remove from the
|
@@ -43,14 +42,12 @@ module Lesli
|
|
43
42
|
|
44
43
|
validates :name, presence: true
|
45
44
|
|
46
|
-
after_create :initialize_descriptor_privileges
|
47
|
-
|
48
45
|
def initialize_descriptor_privileges
|
49
46
|
|
50
47
|
descriptor_operator = DescriptorPrivilegeOperator.new(self)
|
51
48
|
|
52
49
|
descriptor_operator.add_profile_privileges(self) if self.name == "profile"
|
53
|
-
|
50
|
+
|
54
51
|
descriptor_operator.add_owner_privileges(self) if ["owner", "sysadmin"].include?(self.name)
|
55
52
|
|
56
53
|
end
|
@@ -35,11 +35,11 @@ module Lesli
|
|
35
35
|
belongs_to :role
|
36
36
|
belongs_to :descriptor
|
37
37
|
|
38
|
-
|
39
|
-
after_destroy :synchronize_privileges
|
38
|
+
after_commit :synchronize_privileges
|
39
|
+
#after_destroy :synchronize_privileges
|
40
40
|
|
41
41
|
def synchronize_privileges
|
42
|
-
|
42
|
+
RoleDescriptorOperator.new(self.role.id).synchronize
|
43
43
|
end
|
44
44
|
|
45
45
|
def self.index current_user, query, role
|
@@ -17,7 +17,7 @@ GNU General Public License for more details.
|
|
17
17
|
You should have received a copy of the GNU General Public License
|
18
18
|
along with this program. If not, see http://www.gnu.org/licenses/.
|
19
19
|
|
20
|
-
Lesli · Ruby on Rails Development
|
20
|
+
Lesli · Ruby on Rails SaaS Development Framework.
|
21
21
|
|
22
22
|
Made with ♥ by https://www.lesli.tech
|
23
23
|
Building a better future, one line of code at a time.
|
@@ -27,7 +27,7 @@ Building a better future, one line of code at a time.
|
|
27
27
|
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
28
28
|
|
29
29
|
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
30
|
-
// ·
|
30
|
+
// ·
|
31
31
|
=end
|
32
32
|
|
33
33
|
require "bcrypt"
|
@@ -40,13 +40,13 @@ module Lesli
|
|
40
40
|
|
41
41
|
enum session_sources: {
|
42
42
|
dispatcher_standar_session: "dispatcher_standar_session",
|
43
|
-
|
43
|
+
devise_standard_session: "devise_standar_session",
|
44
44
|
cloud_shared_public: "cloud_shared_public",
|
45
45
|
}
|
46
46
|
|
47
47
|
def set_session_token
|
48
48
|
|
49
|
-
return if self.session_source == "
|
49
|
+
return if self.session_source == "devise_standard_session"
|
50
50
|
|
51
51
|
return unless self.session_token.blank?
|
52
52
|
|
@@ -67,29 +67,6 @@ module Lesli
|
|
67
67
|
|
68
68
|
end
|
69
69
|
|
70
|
-
def self.index(current_user, query, params, current_session_id)
|
71
|
-
user_id = params[:user_id]
|
72
|
-
|
73
|
-
User::Session.all
|
74
|
-
.joins(:user)
|
75
|
-
.where(user_id: user_id)
|
76
|
-
.where("users.account_id = ?", current_user.account.id)
|
77
|
-
.where("expiration_at > ? or expiration_at is ?", Time.now.utc, nil)
|
78
|
-
.select(
|
79
|
-
:id,
|
80
|
-
:user_agent,
|
81
|
-
:session_source,
|
82
|
-
Date2.new.date_time.db_timestamps("user_sessions"),
|
83
|
-
Date2.new.date_time.db_column("expiration_at"),
|
84
|
-
Date2.new.date_time.db_column("last_used_at"),
|
85
|
-
"case when #{current_session_id} = user_sessions.id then true else false end as current_session"
|
86
|
-
)
|
87
|
-
.page(query[:pagination][:page])
|
88
|
-
.per(query[:pagination][:perPage])
|
89
|
-
.order(updated_at: :desc)
|
90
|
-
|
91
|
-
end
|
92
|
-
|
93
70
|
def active?
|
94
71
|
if self.deleted_at.present?
|
95
72
|
return false
|
data/app/models/lesli/user.rb
CHANGED
@@ -32,7 +32,6 @@ Building a better future, one line of code at a time.
|
|
32
32
|
|
33
33
|
module Lesli
|
34
34
|
class User < ApplicationLesliRecord
|
35
|
-
|
36
35
|
include UserSecurity
|
37
36
|
include UserExtensions
|
38
37
|
#include UserActivities
|
@@ -88,7 +87,7 @@ module Lesli
|
|
88
87
|
|
89
88
|
# callbacks
|
90
89
|
before_create :before_create_user
|
91
|
-
|
90
|
+
after_create :after_confirmation_user, if: :confirmed?
|
92
91
|
#after_create :after_account_assignation
|
93
92
|
#after_update :update_associated_services
|
94
93
|
|
@@ -116,7 +115,7 @@ module Lesli
|
|
116
115
|
self.set_alias
|
117
116
|
|
118
117
|
# create user details
|
119
|
-
|
118
|
+
User::Detail.find_or_create_by({ user: self })
|
120
119
|
|
121
120
|
# Minimum security settings required
|
122
121
|
self.settings.create_with(:value => false).find_or_create_by(:name => "mfa_enabled")
|
@@ -65,7 +65,7 @@ module Lesli
|
|
65
65
|
def add_owner_privileges(descriptor)
|
66
66
|
|
67
67
|
# Adding default system actions for profile descriptor
|
68
|
-
actions = SystemController::Action.all
|
68
|
+
actions = SystemController::Action.all
|
69
69
|
|
70
70
|
actions.each do |action|
|
71
71
|
descriptor.privileges.find_or_create_by(action: action)
|
@@ -31,7 +31,7 @@ Building a better future, one line of code at a time.
|
|
31
31
|
=end
|
32
32
|
|
33
33
|
module Lesli
|
34
|
-
class
|
34
|
+
class RoleDescriptorOperator < Lesli::ApplicationLesliService
|
35
35
|
|
36
36
|
@roles;
|
37
37
|
|
@@ -48,6 +48,12 @@ module Lesli
|
|
48
48
|
# is get the controllers and actions assigned to a descriptor through the
|
49
49
|
# system_descriptor_privileges table and create an array of hashes with
|
50
50
|
# all the raw privileges (this includes duplicated privileges)
|
51
|
+
# IMPORTANT: A descriptor privilege is evaluated as active if:
|
52
|
+
# - the role has assigned the descriptor through the power association
|
53
|
+
# - the descriptor has assigned the privilege through the controller actions table
|
54
|
+
# - the power has active that group of actions, this means that, if the power has
|
55
|
+
# not marked as active the pshow, pindex, etc column the power is not active
|
56
|
+
# even if it is assigned and active to a descriptor
|
51
57
|
records = Descriptor.joins(%(
|
52
58
|
INNER JOIN lesli_descriptor_privileges
|
53
59
|
ON lesli_descriptor_privileges.descriptor_id = lesli_descriptors.id
|
@@ -60,12 +66,23 @@ module Lesli
|
|
60
66
|
)).joins(%(
|
61
67
|
INNER JOIN lesli_role_powers
|
62
68
|
ON lesli_role_powers.descriptor_id = lesli_descriptors.id
|
63
|
-
)).select(
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
+
)).select(%(
|
70
|
+
lesli_system_controllers.route as controller,
|
71
|
+
lesli_system_controller_actions.name as action,
|
72
|
+
case
|
73
|
+
when lesli_role_powers.deleted_at is not null then false
|
74
|
+
when NULLIF(lesli_system_controller_actions.name = 'list' and lesli_role_powers.plist = true, false) then true
|
75
|
+
when NULLIF(lesli_system_controller_actions.name = 'index' and lesli_role_powers.pindex = true, false) then true
|
76
|
+
when NULLIF(lesli_system_controller_actions.name = 'show' and lesli_role_powers.pshow = true, false) then true
|
77
|
+
when NULLIF(lesli_system_controller_actions.name = 'new' and lesli_role_powers.pcreate = true, false) then true
|
78
|
+
when NULLIF(lesli_system_controller_actions.name = 'create' and lesli_role_powers.pcreate = true, false) then true
|
79
|
+
when NULLIF(lesli_system_controller_actions.name = 'edit' and lesli_role_powers.pupdate = true, false) then true
|
80
|
+
when NULLIF(lesli_system_controller_actions.name = 'update' and lesli_role_powers.pupdate = true, false) then true
|
81
|
+
when NULLIF(lesli_system_controller_actions.name = 'destroy' and lesli_role_powers.pdestroy = true, false) then true
|
82
|
+
else false
|
83
|
+
end as active,
|
84
|
+
lesli_role_powers.role_id as role_id
|
85
|
+
)).with_deleted
|
69
86
|
|
70
87
|
|
71
88
|
# get privileges only for the given role, this is needed to sync only modified roles
|
@@ -34,11 +34,18 @@ Building a better future, one line of code at a time.
|
|
34
34
|
|
35
35
|
<%
|
36
36
|
|
37
|
+
# Should I move this to a base controller?
|
38
|
+
|
37
39
|
# add company information (account)
|
38
40
|
@lesli[:company] = {
|
39
41
|
name: current_user.account.company_name,
|
40
42
|
}
|
41
43
|
|
44
|
+
# set user information
|
45
|
+
@lesli[:current_user] = {
|
46
|
+
id: current_user.id
|
47
|
+
}
|
48
|
+
|
42
49
|
|
43
50
|
# Include defaults to account
|
44
51
|
@lesli[:instance] = Rails.application.credentials.dig(:instance) || lesli_instance_code
|
@@ -49,9 +49,9 @@ Building a better future, one line of code at a time.
|
|
49
49
|
|
50
50
|
<%# Productivity & teamwork %>
|
51
51
|
<%= navigation_engine_driver %>
|
52
|
+
<%= navigation_engine_letter %>
|
52
53
|
<%= navigation_engine_work %>
|
53
54
|
<%= navigation_engine_focus %>
|
54
|
-
<%= navigation_engine_text %>
|
55
55
|
<%= navigation_engine_social %>
|
56
56
|
<%= navigation_engine_bell %>
|
57
57
|
<%= navigation_engine_time %>
|
@@ -64,9 +64,9 @@ Building a better future, one line of code at a time.
|
|
64
64
|
|
65
65
|
<%# IT & Help desk %>
|
66
66
|
<%= navigation_engine_kb %>
|
67
|
-
<%= navigation_engine_help %>
|
68
67
|
<%= navigation_engine_portal %>
|
69
68
|
<%= navigation_engine_shared %>
|
69
|
+
<%= navigation_engine_support %>
|
70
70
|
|
71
71
|
<%# Security & privacy %>
|
72
72
|
<%= navigation_engine_audit %>
|