lesli_shield 1.0.2 → 1.0.4

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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/lesli_shield/confirmations.css +18763 -0
  3. data/app/assets/stylesheets/lesli_shield/devise/oauth.css +32 -0
  4. data/app/assets/stylesheets/lesli_shield/passwords.css +18717 -1
  5. data/app/assets/stylesheets/lesli_shield/registrations.css +18804 -1
  6. data/app/assets/stylesheets/lesli_shield/sessions.css +18804 -1
  7. data/app/assets/stylesheets/lesli_shield/users.css +30 -0
  8. data/app/controllers/lesli_shield/dashboards_controller.rb +1 -8
  9. data/app/controllers/lesli_shield/invites_controller.rb +80 -0
  10. data/app/controllers/lesli_shield/role/actions_controller.rb +32 -20
  11. data/app/controllers/lesli_shield/roles_controller.rb +16 -8
  12. data/app/controllers/lesli_shield/sessions_controller.rb +5 -8
  13. data/app/controllers/lesli_shield/user/roles_controller.rb +62 -0
  14. data/app/controllers/lesli_shield/users_controller.rb +57 -20
  15. data/app/controllers/users/confirmations_controller.rb +42 -8
  16. data/app/controllers/users/passwords_controller.rb +52 -37
  17. data/app/controllers/users/registrations_controller.rb +2 -8
  18. data/app/controllers/users/sessions_controller.rb +57 -50
  19. data/app/helpers/lesli_shield/invites_helper.rb +4 -0
  20. data/app/helpers/lesli_shield/user/roles_helper.rb +4 -0
  21. data/app/interfaces/lesli_shield/authorization_interface.rb +8 -2
  22. data/app/mailers/lesli_shield/devise_mailer.rb +98 -0
  23. data/app/mailers/lesli_shield/invitation.html.erb +23 -0
  24. data/app/models/concerns/lesli_shield/user_security.rb +222 -0
  25. data/app/models/lesli_shield/account.rb +1 -1
  26. data/app/models/lesli_shield/dashboard.rb +1 -4
  27. data/app/models/lesli_shield/invite.rb +24 -0
  28. data/{lib/vue/confirmations.js → app/models/lesli_shield/role/action.rb} +17 -10
  29. data/{db/migrate/v1/0801003010_create_lesli_shield_dashboards.rb → app/models/lesli_shield/role/privilege.rb} +5 -4
  30. data/app/models/lesli_shield/user/role.rb +8 -0
  31. data/app/models/lesli_shield/user/session.rb +80 -0
  32. data/app/services/lesli_shield/invite_service.rb +43 -0
  33. data/app/services/lesli_shield/role_action_service.rb +118 -0
  34. data/app/services/lesli_shield/role_privilege_service.rb +112 -0
  35. data/app/{operators/lesli_shield/user_registration_operator.rb → services/lesli_shield/user_registration_service.rb} +26 -29
  36. data/app/services/lesli_shield/user_session_service.rb +78 -0
  37. data/app/services/lesli_shield/user_validator_service.rb +221 -0
  38. data/app/views/devise/confirmations/show.html.erb +4 -6
  39. data/app/views/devise/passwords/edit.html.erb +1 -2
  40. data/app/views/devise/passwords/new.html.erb +1 -1
  41. data/app/views/devise/registrations/new.html.erb +5 -4
  42. data/app/views/devise/sessions/new.html.erb +3 -2
  43. data/app/views/devise/shared/_application-devise-simple.erb +59 -0
  44. data/app/views/devise/shared/_application-devise.html.erb +76 -0
  45. data/app/views/lesli_shield/dashboards/_component-calendar.html.erb +1 -0
  46. data/app/views/lesli_shield/dashboards/_component-chart-bar.html.erb +6 -0
  47. data/app/views/lesli_shield/dashboards/_component-chart-line.html.erb +8 -0
  48. data/app/views/lesli_shield/dashboards/_component-count.html.erb +1 -0
  49. data/app/views/lesli_shield/dashboards/_component-date.html.erb +1 -0
  50. data/app/views/lesli_shield/dashboards/_component-weather.html.erb +1 -0
  51. data/app/views/lesli_shield/invites/_form.html.erb +10 -0
  52. data/app/views/lesli_shield/invites/_invite.html.erb +2 -0
  53. data/app/views/lesli_shield/invites/edit.html.erb +12 -0
  54. data/app/views/lesli_shield/invites/index.html.erb +66 -0
  55. data/{db/migrate/v1/0801001710_create_lesli_shield_settings.rb → app/views/lesli_shield/invites/new.html.erb} +9 -10
  56. data/{lib/vue/apps/dashboards/components/engine-version.vue → app/views/lesli_shield/invites/show.html.erb} +26 -43
  57. data/app/views/lesli_shield/partials/_navigation.html.erb +2 -4
  58. data/app/views/lesli_shield/{roles/_form-privileges.html.erb → role/actions/_form.html.erb} +5 -30
  59. data/app/views/lesli_shield/role/actions/index.html.erb +14 -0
  60. data/app/views/lesli_shield/roles/index.html.erb +2 -6
  61. data/app/views/lesli_shield/roles/new.html.erb +0 -11
  62. data/app/views/lesli_shield/roles/show.html.erb +5 -8
  63. data/app/views/lesli_shield/user/roles/_form.html.erb +17 -0
  64. data/app/views/lesli_shield/user/roles/_role.html.erb +2 -0
  65. data/app/views/lesli_shield/user/roles/edit.html.erb +12 -0
  66. data/app/views/lesli_shield/user/roles/index.html.erb +16 -0
  67. data/app/views/lesli_shield/user/roles/new.html.erb +11 -0
  68. data/app/views/lesli_shield/user/roles/show.html.erb +10 -0
  69. data/app/views/lesli_shield/users/{_viewer-activities.html.erb → _activities-viewer.html.erb} +2 -4
  70. data/app/views/lesli_shield/users/_information-card.html.erb +3 -3
  71. data/app/views/lesli_shield/users/_management-privileges.html.erb +74 -0
  72. data/app/views/lesli_shield/users/_management-security.html.erb +5 -0
  73. data/app/views/lesli_shield/users/index.html.erb +3 -7
  74. data/app/views/lesli_shield/users/new.html.erb +5 -11
  75. data/app/views/lesli_shield/users/show.html.erb +7 -5
  76. data/config/initializers/devise.rb +305 -304
  77. data/config/locales/translations.en.yml +4 -1
  78. data/config/locales/translations.es.yml +4 -1
  79. data/config/locales/translations.it.yml +4 -1
  80. data/config/routes.rb +7 -8
  81. data/db/migrate/v1/0801100210_create_lesli_shield_role_actions.rb +48 -0
  82. data/db/migrate/v1/0801100410_create_lesli_shield_role_privileges.rb +45 -0
  83. data/db/migrate/v1/0801110110_create_lesli_shield_user_roles.rb +43 -0
  84. data/db/migrate/v1/0801111210_create_lesli_shield_user_sessions.rb +56 -0
  85. data/db/migrate/v1/0801120110_create_lesli_shield_invites.rb +49 -0
  86. data/lib/lesli_shield/engine.rb +3 -3
  87. data/lib/lesli_shield/router.rb +21 -0
  88. data/lib/lesli_shield/version.rb +2 -2
  89. data/lib/lesli_shield.rb +1 -1
  90. data/lib/scss/_devise.scss +10 -0
  91. data/lib/scss/confirmations.scss +24 -24
  92. data/lib/tasks/lesli_shield_tasks.rake +1 -1
  93. data/readme.md +59 -20
  94. metadata +69 -44
  95. data/app/controllers/lesli_shield/dashboard/components_controller.rb +0 -60
  96. data/app/models/lesli_shield/dashboard/component.rb +0 -18
  97. data/app/views/lesli_shield/dashboards/edit.html.erb +0 -1
  98. data/app/views/lesli_shield/dashboards/index.html.erb +0 -9
  99. data/app/views/lesli_shield/dashboards/new.html.erb +0 -1
  100. data/app/views/lesli_shield/dashboards/show.html.erb +0 -1
  101. data/app/views/lesli_shield/roles/_session.html.erb +0 -2
  102. data/app/views/lesli_shield/roles/edit.html.erb +0 -12
  103. data/app/views/lesli_shield/roles/update.turbo_stream.erb +0 -3
  104. data/app/views/lesli_shield/users/update.turbo_stream.erb +0 -3
  105. data/lib/lesli_shield/routing.rb +0 -23
  106. data/lib/vue/application.js +0 -83
  107. data/lib/vue/apps/sessions/index.vue +0 -50
  108. data/lib/vue/passwords.js +0 -137
  109. data/lib/vue/registrations.js +0 -144
  110. data/lib/vue/sessions.js +0 -148
  111. data/lib/vue/stores/sessions.js +0 -43
  112. data/lib/vue/stores/translations.json +0 -162
  113. /data/app/views/lesli_shield/roles/{_form-information.html.erb → _form.html.erb} +0 -0
  114. /data/db/migrate/v1/{0801120310_create_lesli_shield_user_shortcuts.rb → 0801111010_create_lesli_shield_user_shortcuts.rb} +0 -0
  115. /data/db/migrate/v1/{0801120410_create_lesli_shield_user_tokens.rb → 0801111110_create_lesli_shield_user_tokens.rb} +0 -0
@@ -30,85 +30,92 @@ Building a better future, one line of code at a time.
30
30
  // ·
31
31
  =end
32
32
 
33
- #require "uri"
34
-
35
33
  class Users::SessionsController < Devise::SessionsController
36
34
 
37
- # Creates a new session for the user and allows them access to the platform
35
+ # Creates a new session for the user and allows them access to the platform.
36
+ #
37
+ # Devise provides extension points such as Warden hooks and a custom FailureApp
38
+ # to modify the authentication flow. However, in this case we need full and
39
+ # explicit control over each step of the login process, including validation,
40
+ # logging, session creation, and redirection.
41
+ #
42
+ # For that reason, the default Devise session logic is intentionally overridden
43
+ # and reimplemented here. While this approach is less conventional and may
44
+ # introduce compatibility risks with future Devise releases, it provides a
45
+ # predictable and fully controlled authentication pipeline, which is important
46
+ # for the needs of this framework.
47
+ #
48
+ # This trade-off is accepted as part of the framework’s lifecycle: any
49
+ # incompatibilities introduced by Devise updates will be addressed and
50
+ # maintained as needed.
38
51
  def create
39
52
 
40
- # search for a existing user
41
- user = ::Lesli::User.find_for_database_authentication(email: sign_in_params[:email])
53
+ # Use guarden to check if the users credetials are valid
54
+ self.resource = warden.authenticate(auth_options)
42
55
 
43
- # respond with a no valid credentials generic error if not valid user found
44
- unless user
45
- danger(I18n.t("lesli.users/sessions.message_invalid_credentials"))
46
- redirect_to user_session_path(:r => sign_in_params[:redirect]) and return
56
+ # respond with a no valid credentials generic error if warden
57
+ # cannot validate the user
58
+ unless resource
59
+ danger(I18n.t("lesli_shield.devise/sessions.message_not_valid_credentials"))
60
+ redirect_to user_session_path(r: sign_in_params[:redirect]) and return
47
61
  end
48
62
 
49
- # save a invalid credentials log for the requested user
50
- activity = user.activities.new({ title: "session_create", description:"atempt" })
63
+ user = resource
51
64
 
52
- # check password validation
53
- unless user.valid_password?(sign_in_params[:password])
65
+ # check if user has a valid account
66
+ unless user.account
67
+ danger(I18n.t("lesli_shield.devise/sessions.message_not_confirmed_account"))
68
+ redirect_to user_session_path(r: sign_in_params[:redirect]) and return
69
+ end
54
70
 
55
- # save a invalid credentials log for the requested user
56
- activity.update(description: "invalid_credentials")
71
+ log = nil
57
72
 
58
- # respond with a no valid credentials generic error if not valid user found
59
- danger(I18n.t("lesli.users/sessions.message_invalid_credentials"))
60
- redirect_to user_session_path(:r => sign_in_params[:redirect]) and return
61
- end
73
+ # Save a log for the current login attempt
74
+ log = user.log(
75
+ engine: LesliShield,
76
+ source: self.class.name,
77
+ action: action_name,
78
+ operation: 'session_new',
79
+ description: 'Session creation attempt'
80
+ ) if defined?(LesliAudit)
62
81
 
63
82
  # check if user meet requirements to create a new session
64
- Lesli::UsersValidator.new(user).valid? do |valid, failures|
83
+ LesliShield::UserValidatorService.new(user).valid? do |valid, failures|
65
84
 
66
85
  # if user do not meet requirements to login
67
86
  unless valid
68
87
 
69
- activity.update(description: failures.join(", "))
70
-
71
- danger(failures.join(", "))
72
- redirect_to user_session_path(:r => sign_in_params[:redirect]) and return
88
+ failures_string = failures.join(", ")
89
+ danger(failures_string)
90
+ log.update(description: failures_string) if log
91
+ redirect_to user_session_path(r: sign_in_params[:redirect]) and return
73
92
  end
74
93
  end
75
94
 
76
-
77
- # remember the user (not enabled by default)
78
- # remember_me(user) if sign_in_params[:remember_me] == '1'
79
-
80
-
81
95
  # create a new session for the user
82
- current_session = Lesli::User::SessionService.new(user)
83
- .create(get_user_agent(false), request.remote_ip)
96
+ current_session = LesliShield::UserSessionService.new(user)
97
+ .create(request.remote_ip, (get_user_agent(false) if log))
84
98
  .result
85
99
 
86
100
  # make session id globally available
87
101
  session[:user_session_id] = current_session[:id]
88
102
 
89
- # create a new multi factor authentication service instance for the current user
90
- #mfa_service = User::MfaService.new(user, log)
91
-
92
- # generate a new mfa for the current session (if enabled)
93
- #mfa_service.generate do |success|
94
- # mfa was successfully generated, return the user to the mfa page
95
- # return respond_with_successful({ default_path: "mfa" }) if success
96
- #end
97
-
98
103
  # do a user login
99
- sign_in(:user, user)
104
+ sign_in(resource_name, user)
100
105
 
101
- # create a log for login atempts
102
- activity.update({
103
- title: "session_create",
104
- description: "successful",
105
- session_id: current_session[:id]
106
- })
106
+ # update logs with a successful login
107
+ log.update(
108
+ description: "Session creation successful",
109
+ session_id: current_session[:id]
110
+ ) if log
107
111
 
108
112
  # respond successful and send the path user should go
109
- #respond_with_successful({ default_path: user.has_role_with_default_path?() })
110
- #respond_with_successful({ default_path: Lesli.config.path_after_login || "/" })
111
- redirect_to(safe_redirect_path(sign_in_params[:redirect]))
113
+ # respond_with_successful({ default_path: user.has_role_with_default_path?() })
114
+ # respond_with_successful({ default_path: Lesli.config.path_after_login || "/" })
115
+ redirect_to safe_redirect_path(sign_in_params[:redirect])
116
+
117
+ # Save the user_agent for every new session
118
+ log_devices if log
112
119
  end
113
120
 
114
121
  private
@@ -0,0 +1,4 @@
1
+ module LesliShield
2
+ module InvitesHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module LesliShield
2
+ module User::RolesHelper
3
+ end
4
+ end
@@ -27,12 +27,18 @@ module LesliShield
27
27
 
28
28
  # privilege for object not found
29
29
  if granted.blank?
30
- current_user.activities.create({ title: "privilege_not_found", description: request.path })
30
+ log(
31
+ :operation => :authorize_request,
32
+ :description => "Privilege not found for: #{request.path}"
33
+ )
31
34
  return respond_with_unauthorized({ controller: params[:controller], action: params[:action] })
32
35
  end
33
36
 
34
37
  unless granted
35
- current_user.activities.create({ title: "privilege_not_granted", description: request.path })
38
+ log(
39
+ :operation => :authorize_request,
40
+ :description => "Privilege not granted for: #{request.path}"
41
+ )
36
42
  return respond_with_unauthorized({ controller: params[:controller], action: params[:action] })
37
43
  end
38
44
  end
@@ -0,0 +1,98 @@
1
+ =begin
2
+
3
+ Lesli
4
+
5
+ Copyright (c) 2026, 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 LesliTech
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
+ module LesliShield
33
+ class DeviseMailer < Lesli::ApplicationLesliMailer
34
+
35
+ # Sends an email with instructions to allow the user reset the password
36
+ def reset_password_instructions(user, token, opts = {})
37
+
38
+ # defaults for new accounts/users
39
+ email_template = "devise/reset_password_instructions"
40
+ email_subject = I18n.t("core.users/confirmations.mailer_email_verification")
41
+
42
+ # email parameters
43
+ params = {
44
+ url: build_url("/password/edit", { reset_password_token: token}),
45
+ full_name: user.full_name
46
+ }
47
+
48
+ # send email
49
+ email(
50
+ params,
51
+ to: user.email,
52
+ subject: email_subject,
53
+ template_name: email_template
54
+ )
55
+ end
56
+
57
+ # Sends an email to allow the user confirm the email address
58
+ def confirmation_instructions(user, token, opts = {})
59
+
60
+ # defaults for new accounts/users
61
+ email_template = "devise/confirmation_instructions"
62
+ email_subject = I18n.t("core.users/confirmations.mailer_email_verification")
63
+
64
+ # # custom email and subject if user is changin his email address
65
+ # if !record.unconfirmed_email.blank?
66
+ # email_template = "update_email_confirmation_instructions"
67
+ # email_subject = I18n.t("core.users/confirmations.mailer_confirmation_instructions_subject")
68
+ # end
69
+
70
+ # Depending on wheter there is a new user or they are changing their email,
71
+ # one or another field will be used
72
+ email_recipient = user.unconfirmed_email || user.email
73
+
74
+ # email parameters
75
+ params = {
76
+ url: build_url("/confirmation", { confirmation_token: token})
77
+ }
78
+
79
+ # send email
80
+ email(
81
+ params,
82
+ to: email_recipient,
83
+ subject: email_subject,
84
+ template_name: email_template
85
+ )
86
+ end
87
+
88
+ def welcome(user)
89
+ email_recipient = user.unconfirmed_email || user.email
90
+ email(
91
+ { :user => user },
92
+ to: email_recipient,
93
+ subject: "test",
94
+ template_name: "devise/welcome"
95
+ )
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,23 @@
1
+ <!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
2
+ body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
3
+ table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
4
+ img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
5
+ p { display:block;margin:13px 0; }</style><!--[if mso]>
6
+ <noscript>
7
+ <xml>
8
+ <o:OfficeDocumentSettings>
9
+ <o:AllowPNG/>
10
+ <o:PixelsPerInch>96</o:PixelsPerInch>
11
+ </o:OfficeDocumentSettings>
12
+ </xml>
13
+ </noscript>
14
+ <![endif]--><!--[if lte mso 11]>
15
+ <style type="text/css">
16
+ .mj-outlook-group-fix { width:100% !important; }
17
+ </style>
18
+ <![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
19
+ .mj-column-per-100 { width:100% !important; max-width: 100%; }
20
+ }</style><style media="screen and (min-width:480px)">.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }</style><style type="text/css">@media only screen and (max-width:479px) {
21
+ table.mj-full-width-mobile { width: 100% !important; }
22
+ td.mj-full-width-mobile { width: auto !important; }
23
+ }</style><style type="text/css"></style></head><body style="word-spacing:normal;background-color:#ebecf0;"><div style="background-color:#ebecf0;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tbody><tr><td align="center" style="font-size:0px;padding:0px;padding-top:60px;padding-bottom:0px;padding-left:5px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"><tbody><tr><td style="width:125px;"><img src="https://cdn.lesli.tech/leslicloud/brand/app-logo.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="125" height="auto"></td></tr></tbody></table></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--><div style="height:5px;line-height:5px;">&#8202;</div><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:center;color:#4a5056;"><h1>Welcome to The Lesli Family!</h1></div><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;margin:0px auto;border-radius:15px;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;border-radius:15px;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px;text-align:center;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:520px;" ><![endif]--><div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tbody><tr><td align="center" style="font-size:0px;padding:0px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:center;color:#4a5056;"><h2><raw><% name = @data.dig(:user, :full_name) %></raw><raw><%= !name.blank? ? "Hi #{name.strip}." : nil %></raw></h2></div></td></tr><tr><td style="background:#95a3ab;font-size:0px;word-break:break-word;"><div style="height:2px;line-height:2px;">&#8202;</div></td></tr><tr><td style="font-size:0px;word-break:break-word;"><div style="height:5px;line-height:5px;">&#8202;</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;line-height:1.4;text-align:center;color:#4a5056;"><p>You have been invitated to Lesli, please confirm your email address by clicking the button below:</p></div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tbody><tr><td align="center" bgcolor="#eef6fc" role="presentation" style="border:1px solid #209cee;border-radius:5px;cursor:auto;mso-padding-alt:10px;background:#eef6fc;" valign="middle"><a href="<%= url_for(@app[:host] + @data[:url]) %>" style="display:inline-block;background:#eef6fc;color:#209cee;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:17px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px;mso-padding-alt:0px;border-radius:5px;" target="_blank">Confirm my account</a></td></tr></tbody></table></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--><div style="height:30px;line-height:30px;">&#8202;</div><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tbody><tr><td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:600;line-height:1;text-align:center;color:#444444;">¡Siguenos!</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"><table cellpadding="0" cellspacing="0" width="300" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:300px;border:none;"><tr><td align="center"><img width="45px" alt="Facebook" src="https://cdn.lesli.tech/leslicloud/emails/social-facebook.png"></td><td align="center"><img width="45px" alt="Twitter" src="https://cdn.lesli.tech/leslicloud/emails/social-twitter.png"></td><td align="center"><img width="45px" alt="Instagram" src="https://cdn.lesli.tech/leslicloud/emails/social-instagram.png"></td><td align="center"><img width="45px" alt="Linkedin" src="https://cdn.lesli.tech/leslicloud/emails/social-linkedin.png"></td></tr></table></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tbody><tr><td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:600;line-height:1;text-align:center;color:#444444;">Download our app</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"><table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;"><tr><td style="text-align: right; padding: 5px;"><a target="blank" href="https://apps.apple.com/us/app/lesli/id1595893730"><img width="130px" alt="Appstore badge" src="https://cdn.lesli.tech/leslicloud/emails/appstore.png"></a></td><td style="text-align: left; padding: 5px;"><a target="blank" href="https://apps.apple.com/us/app/lesli/id1595893730"><img width="130px" alt="Playstore badge" src="https://cdn.lesli.tech/leslicloud/emails/playstore.png"></a></td></tr></table></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--><div style="height:10px;line-height:10px;">&#8202;</div><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tbody><tr><td align="center" style="font-size:0px;padding:4px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:center;color:#666666;">Copyright &copy; <%= Time.new.year %> LesliTech</div></td></tr><tr><td align="center" style="font-size:0px;padding:4px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:center;color:#666666;">Ciudad de Guatemala, Guatemala.</div></td></tr><tr><td align="center" style="font-size:0px;padding:4px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:center;color:#666666;">All rights reserved.</div></td></tr><tr><td align="center" style="font-size:0px;padding:4px;padding-top:20px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:center;color:#666666;">The Lesli Team</div></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
@@ -0,0 +1,222 @@
1
+ =begin
2
+
3
+ Lesli
4
+
5
+ Copyright (c) 2025, 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 LesliTech
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
+
34
+ # User extension methods
35
+ # Custom methods that belongs to a instance user
36
+ module LesliShield
37
+ module UserSecurity
38
+ extend ActiveSupport::Concern
39
+
40
+ # get the max level permission from roles assigned to the user
41
+ def max_level_permission
42
+ self.roles.maximum(:permission_level) || 0
43
+ end
44
+
45
+ # check if user has roles with specific names
46
+ def has_roles?(*roles)
47
+ !roles.intersection(self.roles.map{ |r| r[:name] }).empty?
48
+ end
49
+
50
+ # check the privilege cache to check if user is able
51
+ # to perform a specific action in a specific controller
52
+ def has_privileges_for?(controller, action)
53
+ begin
54
+ return self.privileges.where(
55
+ controller: controller,
56
+ action: action,
57
+ active: true
58
+ ).exists?
59
+ rescue => exception
60
+ L2.danger(exception.to_s)
61
+ return false
62
+ end
63
+ end
64
+
65
+ # Check if user has enough privilege to work with the given role
66
+ def can_work_with_role?(role_id)
67
+
68
+ # get the role if only id is given
69
+ role = self.account.roles.find_by(:id => role_id)
70
+
71
+ # false if role not found
72
+ return false if role.blank?
73
+
74
+ # not valid role without object levelpermission defined
75
+ return false if role.level_permission.blank?
76
+
77
+ # get the max level permission from the roles the user has assigned
78
+ user_role_max_level_permission = self.roles.map(&:level_permission).max()
79
+
80
+ # check if user can work with the level permission of the role is trying to modify
81
+ # Note: user only can assigned an level permission below the max of his own roles
82
+ # Current user cannot assign role if role to assign is the same of the greater role
83
+ # assigned to the current user
84
+ user_role_max_level_permission >= role.level_permission
85
+ end
86
+
87
+ # Checks configuration of all the roles assigned to the user
88
+ # if user has a role with "default path" to use as home to redirect after login
89
+ # IMPORTANT: This home path is used only the send the user after login, the user
90
+ # and the role are not limited by this configuration
91
+ def has_role_with_default_path?()
92
+
93
+ # get the roles that contains a path
94
+ role = self.roles.where.not(path_default: [nil, ""])
95
+
96
+ # here we must order the results descendant because we must
97
+ # keep the path of the hightest level permission role.
98
+ # Example: we should use the path of the admin role if user has
99
+ # admin & employee roles, also order by default_path, so we get first
100
+ # the roles with path in case the user has roles with the same level permission
101
+ role = role.order(level_permission: :desc).order(:path_default)
102
+
103
+ # get the first role found, due previously we sort in a descendant order
104
+ # the first role is going to be the one with highest level permission
105
+ # this is going to return nil if no role was found
106
+ default_path = role.first&.path_default || "/"
107
+
108
+ # if first loggin for account owner send him to the onboarding page
109
+ if self.account.onboarding? && self.has_roles?("owner")
110
+ default_path = "/onboarding"
111
+ end
112
+
113
+ default_path
114
+ end
115
+
116
+ # Checks configuration of all the roles assigned to the user
117
+ # if user has a role limited to a defined path
118
+ # if user has a high privilege role that overrides any other role configuration
119
+ def has_role_limited_to_path?()
120
+
121
+ # get the roles ordering in descendant mode because we must
122
+ # keep the path of the hightest level permission role.
123
+ # Example: we should use the path of the admin role if user has
124
+ # admin & employee roles, also order by default_path, so we get first
125
+ # the roles with path in case the user has roles with the same level permission
126
+ role = self.roles.order(level_permission: :desc).order(:path_default)
127
+
128
+ # get the first role found, due previously we sort in a descendant order
129
+ # the first role is going to be the one with highest level permission
130
+ # this is going to return nil if no role was found
131
+ role = role.first
132
+
133
+ # return the path of the role if is limited to a that specific path
134
+ return role.path_default if role.path_limited == true
135
+
136
+ # return nil if role has no limits
137
+ return nil
138
+ end
139
+
140
+ # Sets this user as inactive and removes complete access to the platform
141
+ def revoke_access
142
+ self.update(active: false)
143
+ end
144
+
145
+ # Change user password forcing user to reset the password
146
+ def set_password_as_expired
147
+ self.update(password_expiration_at: Time.current)
148
+ end
149
+
150
+ # @description Change user password forcing user to reset the password
151
+ def set_password_for_reset
152
+ generate_password_reset_token()
153
+ end
154
+
155
+ def has_expired_password?
156
+ return false if self.password_expiration_at.blank?
157
+ return Time.current > self.password_expiration_at
158
+ end
159
+
160
+ # Check if user has a confirmed telephone number
161
+ def has_telephone_confirmed?
162
+ !!self.telephone_confirmed_at
163
+ end
164
+
165
+ # Change user password forcing user to reset the password
166
+ def generate_password_reset_token
167
+ raw, hashed = Devise.token_generator.generate(Lesli::User, :reset_password_token)
168
+
169
+ self.update!(
170
+ password: nil,
171
+ reset_password_token: hashed,
172
+ reset_password_sent_at: Time.current
173
+ )
174
+
175
+ raw
176
+ end
177
+
178
+ # Generate a token to validate telephone number
179
+ def generate_telephone_token(length=4)
180
+ raw, enc = Devise.token_generator.create(
181
+ self.class,
182
+ :telephone_confirmation_token,
183
+ type:'number',
184
+ length:length
185
+ )
186
+
187
+ self.telephone_confirmation_token = enc
188
+ self.telephone_confirmation_sent_at = Time.now.utc
189
+ self.telephone_confirmed_at = nil
190
+ save(validate: false)
191
+ raw
192
+ end
193
+
194
+ # Mark telephone number as valid and confirmed
195
+ def confirm_telephone_number
196
+ self.telephone_confirmation_token = nil
197
+ self.telephone_confirmation_sent_at = nil
198
+ self.telephone_confirmed_at = Time.now.utc
199
+ save(validate: false)
200
+ end
201
+
202
+ # Return a hash that contains all the abilities grouped by
203
+ # controller and define every action privilege. It also
204
+ # evaluate if the user has the ability no matter if is given
205
+ # to the user by role or by itself.
206
+ def abilities_by_controller
207
+
208
+ # Abilities hash where we will save all the privileges the user has to
209
+ abilities = {}
210
+
211
+ # We check all the privileges the user has in the cache table according to his roles
212
+ # and create a key per controller (with the full controller name) that contains an array of all the
213
+ # methods/actions with permission
214
+ # self.privileges.all.each do |privilege|
215
+ # abilities[privilege.controller] = [] if abilities[privilege.controller].nil?
216
+ # abilities[privilege.controller] << privilege.action
217
+ # end
218
+
219
+ abilities
220
+ end
221
+ end
222
+ end
@@ -34,11 +34,11 @@ module LesliShield
34
34
  class Account < ApplicationRecord
35
35
  belongs_to :account, class_name: "Lesli::Account"
36
36
  has_many :dashboards
37
+ has_many :invites
37
38
 
38
39
  after_create :initialize_account
39
40
 
40
41
  def initialize_account
41
- Dashboard.initialize_account(self)
42
42
  end
43
43
  end
44
44
  end
@@ -32,9 +32,6 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliShield
34
34
  class Dashboard < Lesli::Shared::Dashboard
35
- self.table_name = "lesli_shield_dashboards"
36
- belongs_to :account
37
-
38
- COMPONENTS = %i[]
35
+ COMPONENTS = %i[calendar chart_bar weather]
39
36
  end
40
37
  end
@@ -0,0 +1,24 @@
1
+ module LesliShield
2
+ class Invite < ApplicationRecord
3
+ belongs_to :account
4
+ belongs_to :user, class_name: "Lesli::User"
5
+
6
+ validates :email, presence: true, on: :create
7
+
8
+ before_create :before_create_invite
9
+
10
+ enum :status, {
11
+ created: 0,
12
+ accepted: 1,
13
+ cancelled: 2,
14
+ sent: 5,
15
+ requested: 6
16
+ }
17
+
18
+ private
19
+
20
+ def before_create_invite
21
+ self.status = :created
22
+ end
23
+ end
24
+ end
@@ -1,7 +1,8 @@
1
- /*
1
+ =begin
2
+
2
3
  Lesli
3
4
 
4
- Copyright (c) 2023, Lesli Technologies, S. A.
5
+ Copyright (c) 2026, Lesli Technologies, S. A.
5
6
 
6
7
  This program is free software: you can redistribute it and/or modify
7
8
  it under the terms of the GNU General Public License as published by
@@ -16,18 +17,24 @@ GNU General Public License for more details.
16
17
  You should have received a copy of the GNU General Public License
17
18
  along with this program. If not, see http://www.gnu.org/licenses/.
18
19
 
19
- Lesli · Your Smart Business Assistant.
20
+ Lesli · Ruby on Rails SaaS Development Framework.
20
21
 
21
- Made with ♥ by https://www.lesli.tech
22
+ Made with ♥ by LesliTech
22
23
  Building a better future, one line of code at a time.
23
24
 
24
25
  @contact hello@lesli.tech
25
- @website https://lesli.tech
26
+ @website https://www.lesli.tech
26
27
  @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
27
28
 
28
- // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
29
- // ·
30
- */
31
-
32
-
29
+ // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
33
30
  // ·
31
+ =end
32
+
33
+ module LesliShield
34
+ class Role::Action < Lesli::ApplicationLesliRecord
35
+ self.table_name = 'lesli_shield_role_actions'
36
+
37
+ belongs_to :role, class_name: 'Lesli::Role'
38
+ belongs_to :action, class_name: "Lesli::Resource"
39
+ end
40
+ end