lesli 5.0.5 → 5.0.6
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/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 %>
|