masks 0.2.0

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 (126) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +50 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/builds/application.css +4764 -0
  6. data/app/assets/builds/application.js +8236 -0
  7. data/app/assets/builds/application.js.map +7 -0
  8. data/app/assets/builds/masks/application.css +1 -0
  9. data/app/assets/builds/masks/application.js +7122 -0
  10. data/app/assets/builds/masks/application.js.map +7 -0
  11. data/app/assets/images/masks.png +0 -0
  12. data/app/assets/javascripts/application.js +2 -0
  13. data/app/assets/javascripts/controllers/application.js +9 -0
  14. data/app/assets/javascripts/controllers/emails_controller.js +28 -0
  15. data/app/assets/javascripts/controllers/index.js +12 -0
  16. data/app/assets/javascripts/controllers/keys_controller.js +20 -0
  17. data/app/assets/javascripts/controllers/recover_controller.js +21 -0
  18. data/app/assets/javascripts/controllers/recover_password_controller.js +21 -0
  19. data/app/assets/javascripts/controllers/session_controller.js +94 -0
  20. data/app/assets/manifest.js +2 -0
  21. data/app/assets/masks_manifest.js +2 -0
  22. data/app/assets/stylesheets/application.css +26 -0
  23. data/app/controllers/concerns/masks/controller.rb +114 -0
  24. data/app/controllers/masks/actors_controller.rb +15 -0
  25. data/app/controllers/masks/application_controller.rb +35 -0
  26. data/app/controllers/masks/backup_codes_controller.rb +34 -0
  27. data/app/controllers/masks/debug_controller.rb +9 -0
  28. data/app/controllers/masks/devices_controller.rb +20 -0
  29. data/app/controllers/masks/emails_controller.rb +60 -0
  30. data/app/controllers/masks/error_controller.rb +14 -0
  31. data/app/controllers/masks/keys_controller.rb +45 -0
  32. data/app/controllers/masks/manage/actor_controller.rb +35 -0
  33. data/app/controllers/masks/manage/actors_controller.rb +12 -0
  34. data/app/controllers/masks/manage/base_controller.rb +12 -0
  35. data/app/controllers/masks/one_time_code_controller.rb +49 -0
  36. data/app/controllers/masks/passwords_controller.rb +33 -0
  37. data/app/controllers/masks/recoveries_controller.rb +43 -0
  38. data/app/controllers/masks/sessions_controller.rb +53 -0
  39. data/app/helpers/masks/application_helper.rb +49 -0
  40. data/app/jobs/masks/application_job.rb +7 -0
  41. data/app/jobs/masks/expire_actors_job.rb +15 -0
  42. data/app/jobs/masks/expire_recoveries_job.rb +15 -0
  43. data/app/mailers/masks/actor_mailer.rb +22 -0
  44. data/app/mailers/masks/application_mailer.rb +15 -0
  45. data/app/models/concerns/masks/access.rb +162 -0
  46. data/app/models/concerns/masks/actor.rb +132 -0
  47. data/app/models/concerns/masks/adapter.rb +68 -0
  48. data/app/models/concerns/masks/role.rb +9 -0
  49. data/app/models/concerns/masks/scoped.rb +54 -0
  50. data/app/models/masks/access/actor_password.rb +20 -0
  51. data/app/models/masks/access/actor_scopes.rb +18 -0
  52. data/app/models/masks/access/actor_signup.rb +22 -0
  53. data/app/models/masks/actors/anonymous.rb +40 -0
  54. data/app/models/masks/actors/system.rb +24 -0
  55. data/app/models/masks/adapters/active_record.rb +85 -0
  56. data/app/models/masks/application_model.rb +15 -0
  57. data/app/models/masks/application_record.rb +8 -0
  58. data/app/models/masks/check.rb +192 -0
  59. data/app/models/masks/credential.rb +166 -0
  60. data/app/models/masks/credentials/backup_code.rb +30 -0
  61. data/app/models/masks/credentials/device.rb +59 -0
  62. data/app/models/masks/credentials/email.rb +48 -0
  63. data/app/models/masks/credentials/factor2.rb +71 -0
  64. data/app/models/masks/credentials/key.rb +38 -0
  65. data/app/models/masks/credentials/last_login.rb +12 -0
  66. data/app/models/masks/credentials/masquerade.rb +32 -0
  67. data/app/models/masks/credentials/nickname.rb +63 -0
  68. data/app/models/masks/credentials/one_time_code.rb +34 -0
  69. data/app/models/masks/credentials/password.rb +28 -0
  70. data/app/models/masks/credentials/recovery.rb +71 -0
  71. data/app/models/masks/credentials/session.rb +67 -0
  72. data/app/models/masks/device.rb +30 -0
  73. data/app/models/masks/error.rb +51 -0
  74. data/app/models/masks/event.rb +14 -0
  75. data/app/models/masks/mask.rb +255 -0
  76. data/app/models/masks/rails/actor.rb +190 -0
  77. data/app/models/masks/rails/actor_role.rb +12 -0
  78. data/app/models/masks/rails/device.rb +47 -0
  79. data/app/models/masks/rails/email.rb +96 -0
  80. data/app/models/masks/rails/key.rb +61 -0
  81. data/app/models/masks/rails/recovery.rb +116 -0
  82. data/app/models/masks/rails/role.rb +20 -0
  83. data/app/models/masks/rails/scope.rb +15 -0
  84. data/app/models/masks/session.rb +447 -0
  85. data/app/models/masks/sessions/access.rb +26 -0
  86. data/app/models/masks/sessions/inline.rb +16 -0
  87. data/app/models/masks/sessions/request.rb +42 -0
  88. data/app/resources/masks/actor_resource.rb +9 -0
  89. data/app/resources/masks/session_resource.rb +15 -0
  90. data/app/views/layouts/masks/application.html.erb +17 -0
  91. data/app/views/layouts/masks/mailer.html.erb +17 -0
  92. data/app/views/layouts/masks/mailer.text.erb +1 -0
  93. data/app/views/layouts/masks/manage.html.erb +25 -0
  94. data/app/views/masks/actor_mailer/recover_credentials.html.erb +33 -0
  95. data/app/views/masks/actor_mailer/recover_credentials.text.erb +1 -0
  96. data/app/views/masks/actor_mailer/verify_email.html.erb +34 -0
  97. data/app/views/masks/actor_mailer/verify_email.text.erb +8 -0
  98. data/app/views/masks/actors/current.html.erb +152 -0
  99. data/app/views/masks/application/_header.html.erb +31 -0
  100. data/app/views/masks/backup_codes/new.html.erb +103 -0
  101. data/app/views/masks/emails/new.html.erb +103 -0
  102. data/app/views/masks/emails/verify.html.erb +51 -0
  103. data/app/views/masks/keys/new.html.erb +127 -0
  104. data/app/views/masks/manage/actor/show.html.erb +126 -0
  105. data/app/views/masks/manage/actors/index.html.erb +40 -0
  106. data/app/views/masks/one_time_code/new.html.erb +150 -0
  107. data/app/views/masks/passwords/edit.html.erb +58 -0
  108. data/app/views/masks/recoveries/new.html.erb +71 -0
  109. data/app/views/masks/recoveries/password.html.erb +64 -0
  110. data/app/views/masks/sessions/new.html.erb +153 -0
  111. data/config/brakeman.ignore +28 -0
  112. data/config/locales/en.yml +286 -0
  113. data/config/routes.rb +46 -0
  114. data/db/migrate/20231205173845_create_actors.rb +94 -0
  115. data/lib/generators/masks/install/USAGE +8 -0
  116. data/lib/generators/masks/install/install_generator.rb +33 -0
  117. data/lib/generators/masks/install/templates/initializer.rb +5 -0
  118. data/lib/generators/masks/install/templates/masks.json +6 -0
  119. data/lib/masks/configuration.rb +236 -0
  120. data/lib/masks/engine.rb +25 -0
  121. data/lib/masks/middleware.rb +70 -0
  122. data/lib/masks/version.rb +5 -0
  123. data/lib/masks.rb +183 -0
  124. data/lib/tasks/masks_tasks.rake +71 -0
  125. data/masks.json +274 -0
  126. metadata +416 -0
Binary file
@@ -0,0 +1,2 @@
1
+ import "@hotwired/turbo-rails";
2
+ import "./controllers";
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+
3
+ const application = Application.start();
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false;
7
+ window.Stimulus = application;
8
+
9
+ export { application };
@@ -0,0 +1,28 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return ["email", "password", "submit"];
6
+ }
7
+
8
+ connect() {
9
+ this.email = this.emailTarget.value;
10
+ this.pass = this.passwordTarget.value;
11
+
12
+ this.syncState();
13
+ }
14
+
15
+ updateEmail(e) {
16
+ this.email = e.target.value;
17
+ this.syncState();
18
+ }
19
+
20
+ updatePassword(e) {
21
+ this.pass = e.target.value;
22
+ this.syncState();
23
+ }
24
+
25
+ syncState() {
26
+ this.submitTarget.disabled = !this.email?.includes("@") || !this.pass;
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ import { application } from "./application";
2
+
3
+ import SessionController from "./session_controller";
4
+ import RecoverController from "./recover_controller";
5
+ import RecoverPasswordController from "./recover_password_controller";
6
+ import EmailsController from "./emails_controller";
7
+ import KeysController from "./keys_controller";
8
+ application.register("session", SessionController);
9
+ application.register("recover", RecoverController);
10
+ application.register("recover-password", RecoverPasswordController);
11
+ application.register("emails", EmailsController);
12
+ application.register("keys", KeysController);
@@ -0,0 +1,20 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return ["settings"];
6
+ }
7
+
8
+ connect() {
9
+ // this.email = this.;emailTarget.value;
10
+ // this.pass = this.passwordTarget.value;
11
+ // this.syncState();
12
+ }
13
+
14
+ toggleSettings(e) {
15
+ this.settingsTarget.classList.toggle("hidden");
16
+
17
+ e.preventDefault();
18
+ e.stopPropagation();
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return ["input", "submit"];
6
+ }
7
+
8
+ connect() {
9
+ this.input = this.inputTarget.value;
10
+ this.syncState();
11
+ }
12
+
13
+ updateInput(e) {
14
+ this.input = e.target.value;
15
+ this.syncState();
16
+ }
17
+
18
+ syncState() {
19
+ this.submitTarget.disabled = !this.input?.length;
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return ["password", "submit"];
6
+ }
7
+
8
+ connect() {
9
+ this.password = this.passwordTarget.value;
10
+ this.syncState();
11
+ }
12
+
13
+ updatePassword(e) {
14
+ this.password = e.target.value;
15
+ this.syncState();
16
+ }
17
+
18
+ syncState() {
19
+ this.submitTarget.disabled = !this.password?.length;
20
+ }
21
+ }
@@ -0,0 +1,94 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return [
6
+ "nickname",
7
+ "oneTimeCode",
8
+ "backupCode",
9
+ "password",
10
+ "submit",
11
+ "remember",
12
+ "flash",
13
+ ];
14
+ }
15
+
16
+ connect() {
17
+ this.session = this.hasNicknameTarget
18
+ ? {
19
+ nickname: this.nicknameTarget.value,
20
+ password: this.passwordTarget.value,
21
+ }
22
+ : {};
23
+
24
+ if (this.hasOneTimeCodeTarget) {
25
+ this.session.code = this.oneTimeCodeTarget.value;
26
+ this.session.factor2 = true;
27
+ }
28
+
29
+ if (this.hasBackupCodeTarget) {
30
+ this.session.code = this.backupCodeTarget.value;
31
+ this.session.factor2 = true;
32
+ }
33
+
34
+ this.syncState();
35
+ }
36
+
37
+ updateCode(e) {
38
+ this.session.code = e.target.value;
39
+ this.syncState();
40
+ }
41
+
42
+ updateNickname(e) {
43
+ this.session.nickname = e.target.value;
44
+ this.syncState();
45
+ }
46
+
47
+ updatePassword(e) {
48
+ this.session.password = e.target.value;
49
+ this.syncState();
50
+ }
51
+
52
+ syncState() {
53
+ this.submitTarget.classList.add("btn-accent");
54
+
55
+ let flash;
56
+
57
+ if (this.session.nickname && this.session.password) {
58
+ this.submitTarget.disabled = false;
59
+ this.rememberTarget.classList.remove("hidden");
60
+
61
+ flash = "continue";
62
+ } else if (this.session.factor2) {
63
+ const disabled = (this.session.code?.length || 0) < 6;
64
+ this.submitTarget.disabled = disabled;
65
+
66
+ if (disabled) {
67
+ this.rememberTarget.classList.add("hidden");
68
+ } else {
69
+ this.rememberTarget.classList.remove("hidden");
70
+ }
71
+
72
+ flash = "enter-factor2";
73
+ } else {
74
+ this.submitTarget.disabled = true;
75
+ this.rememberTarget.classList.add("hidden");
76
+
77
+ if (this.session.nickname) {
78
+ flash = "enter-password";
79
+ } else {
80
+ flash = "enter-credentials";
81
+ }
82
+ }
83
+
84
+ if (flash) {
85
+ for (let el of this.flashTarget.querySelectorAll("[data-flash]")) {
86
+ if (el.dataset.flash == flash) {
87
+ el.classList.remove("hidden");
88
+ } else {
89
+ el.classList.add("hidden");
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,2 @@
1
+ //= link_tree ../builds
2
+ //= link_tree ../images
@@ -0,0 +1,2 @@
1
+ //= link_tree ../builds
2
+ //= link_tree ../images
@@ -0,0 +1,26 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+ @tailwind base;
17
+ @tailwind components;
18
+ @tailwind utilities;
19
+
20
+ .pagination {
21
+ @apply flex gap-2 items-center justify-center p-4;
22
+ }
23
+
24
+ .pagination .page {
25
+ @apply btn btn-sm;
26
+ }
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # Helpers for interacting with Masks in Rails controllers.
5
+ #
6
+ # @see ClassMethods
7
+ module Controller
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ # Require a session masked by the type passed.
12
+ #
13
+ # If this fails +unathorized_access+ is called.
14
+ #
15
+ # @param [Symbol|String] type
16
+ # @param [Hash] opts forwarded to +before_action+
17
+ def require_mask(type: nil, **opts)
18
+ before_action(
19
+ :unauthorized_access,
20
+ unless: -> { masked?(type:) },
21
+ **opts
22
+ )
23
+ end
24
+
25
+ # Builds an access object by the passed name, if allowed.
26
+ #
27
+ # If this succeeds, the controller instance's +current_access+ will return
28
+ # the constructed access class. Otherwise, on failure the
29
+ # +unathorized_access+ method is called.
30
+ #
31
+ # @example
32
+ # class MyController < ApplicationController
33
+ # include Masks::Controller
34
+ #
35
+ # require_access 'my.example', only: :index
36
+ #
37
+ # # this action is only called if the session is
38
+ # # able to build the "my.example" access class.
39
+ # def index
40
+ # render json: {
41
+ # foobar: current_access.foo_bar
42
+ # }
43
+ # end
44
+ # end
45
+ #
46
+ # @param [Symbol|String] name
47
+ # @param [Hash] opts forwarded to +before_action+
48
+ def require_access(name, **opts)
49
+ before_action(
50
+ :unauthorized_access,
51
+ unless: -> { build_access(name) },
52
+ **opts
53
+ )
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def passed?
60
+ current_actor && masked_session.passed?
61
+ end
62
+
63
+ def unauthorized_access
64
+ render plain: "", status: :unauthorized
65
+ end
66
+
67
+ # Returns the current masks session for the request.
68
+ #
69
+ # @return [Masks::Session]
70
+ # @visibility public
71
+ def masked_session
72
+ @masked_session ||= request.env[Masks::Middleware::SESSION_KEY]
73
+ end
74
+
75
+ # Returns the mask for the request.
76
+ #
77
+ # @return [Masks::Mask]
78
+ # @visibility public
79
+ def current_mask
80
+ masked_session.mask
81
+ end
82
+
83
+ # Returns the mask for the request.
84
+ #
85
+ # @return [Masks::Actor]
86
+ # @visibility public
87
+ def current_actor
88
+ masked_session.scoped
89
+ end
90
+
91
+ # Returns the mask for the request.
92
+ #
93
+ # @return [Masks::Access]
94
+ # @visibility public
95
+ def current_access
96
+ @current_access
97
+ end
98
+
99
+ def build_access(name)
100
+ @current_access = Masks.access(name, masked_session)
101
+ rescue Masks::Error::Unauthorized
102
+ false
103
+ end
104
+
105
+ def masked?(type: nil)
106
+ if type &&
107
+ !Array.wrap(type).map(&:to_s).include?(masked_session.mask.type)
108
+ false
109
+ else
110
+ masked_session.passed?
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ class ActorsController < ApplicationController
6
+ require_mask type: %i[session api], only: :current
7
+
8
+ def current
9
+ respond_to do |format|
10
+ format.json { render json: ActorResource.new(current_actor) }
11
+ format.html { render(:current) }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ class ApplicationController < ActionController::Base
6
+ include Masks::Controller
7
+ include Pagy::Backend
8
+
9
+ before_action :assign_session
10
+
11
+ skip_before_action :verify_authenticity_token, if: :json_request?
12
+
13
+ protect_from_forgery with: :exception
14
+
15
+ private
16
+
17
+ def json_request?
18
+ request.format.symbol == :json
19
+ end
20
+
21
+ def assign_session
22
+ @session = masked_session
23
+ @config = @session.config
24
+ @actor = @session.actor
25
+ end
26
+
27
+ def require_sudo(redirect)
28
+ return if current_mask.type == "sudo" && passed?
29
+
30
+ flash[:errors] = ["enter a valid password"]
31
+
32
+ redirect_to redirect
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ class BackupCodesController < ApplicationController
6
+ require_mask type: :session, only: :new
7
+ before_action only: %i[create] do
8
+ require_sudo(backup_codes_path)
9
+ end
10
+
11
+ def new
12
+ respond_to { |format| format.html { render(:new) } }
13
+ end
14
+
15
+ def create
16
+ if create_params[:reset]
17
+ @actor.saved_backup_codes_at = nil
18
+ @actor.backup_codes = nil
19
+ @actor.save
20
+ elsif create_params[:enable]
21
+ @actor.saved_backup_codes_at = Time.current
22
+ @actor.save
23
+ end
24
+
25
+ respond_to { |format| format.html { redirect_to backup_codes_path } }
26
+ end
27
+
28
+ private
29
+
30
+ def create_params
31
+ params.require(:backup_codes).permit(:reset, :enable)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ class DebugController < ApplicationController
5
+ def show
6
+ render json: { session: session.to_h }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ class DevicesController < ApplicationController
6
+ require_mask type: :session, only: :update
7
+
8
+ def update
9
+ device =
10
+ masked_session.config.find_device(masked_session, key: params[:key])
11
+
12
+ if device&.persisted? && params[:reset]
13
+ device.reset_version
14
+ device.save!
15
+ end
16
+
17
+ redirect_back_or_to "/"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ class EmailsController < ApplicationController
6
+ require_mask type: :session, only: :new
7
+ before_action :require_sudo, only: :create
8
+
9
+ def new
10
+ @emails = current_actor.emails
11
+
12
+ respond_to { |format| format.html { render(:new) } }
13
+ end
14
+
15
+ def create
16
+ @email = current_actor.emails.build(email: email_param.strip)
17
+
18
+ if @email.valid?
19
+ @email.notify!(masked_session)
20
+ else
21
+ flash[:errors] = @email.errors.full_messages
22
+ end
23
+
24
+ respond_to { |format| format.html { redirect_to emails_path } }
25
+ end
26
+
27
+ def notify
28
+ @email = current_actor.emails.find_by(email: email_param.strip)
29
+ @email.notify!(masked_session) if @email.expired?
30
+
31
+ respond_to { |format| format.html { redirect_to emails_path } }
32
+ end
33
+
34
+ def delete
35
+ @email = current_actor.emails.find_by(email: email_param)
36
+ @email&.destroy
37
+
38
+ respond_to { |format| format.html { redirect_to emails_path } }
39
+ end
40
+
41
+ def verify
42
+ @email = current_actor.emails.find_by(token: params[:email])
43
+ @email&.verify! if @email&.valid?
44
+ end
45
+
46
+ private
47
+
48
+ def email_param
49
+ params.dig(:email, :value)
50
+ end
51
+
52
+ def require_sudo
53
+ return if current_mask.type == "sudo" && passed?
54
+
55
+ flash[:errors] = ["enter a valid password"]
56
+
57
+ redirect_to emails_path
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ class ErrorController < ApplicationController
6
+ def render_unauthorized
7
+ render plain: "", status: 401
8
+ end
9
+
10
+ def redirect
11
+ redirect_to masked_session.mask.fail
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ class KeysController < ApplicationController
6
+ require_mask type: :session
7
+
8
+ def new
9
+ @keys = current_actor.keys
10
+ end
11
+
12
+ def create
13
+ key =
14
+ current_actor.keys.build(
15
+ name: create_params[:name],
16
+ secret: create_params[:secret]&.presence,
17
+ scopes: create_params[:scopes]
18
+ )
19
+ key.save
20
+
21
+ if key.valid?
22
+ flash[:key] = { name: key.name, secret: key.secret }
23
+ else
24
+ flash[:error] = key.errors.full_messages.first
25
+ end
26
+
27
+ redirect_back_or_to keys_path
28
+ end
29
+
30
+ def delete
31
+ key = current_actor.keys.find(params[:id])
32
+ key.destroy
33
+
34
+ flash[:notice] = "#{key.name} destroyed" if key.destroyed?
35
+
36
+ redirect_back_or_to "/"
37
+ end
38
+
39
+ private
40
+
41
+ def create_params
42
+ params.require(:key).permit(:name, :secret, scopes: [])
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ module Manage
6
+ class ActorController < BaseController
7
+ before_action :find_actor
8
+
9
+ def update
10
+ Masks::Rails::Actor.transaction do
11
+ if params[:add_scope]
12
+ @actor.assign_scopes!(params[:add_scope])
13
+ flash[:info] = "added scope"
14
+ elsif params[:remove_scope]
15
+ @actor.remove_scopes!(params[:remove_scope])
16
+ flash[:info] = "removed scope"
17
+ elsif params[:remove_factor2]
18
+ @actor.remove_factor2! if params[:remove_factor2]
19
+ flash[:info] = "removed second factor authentication"
20
+ end
21
+
22
+ redirect_to actor_path(@actor)
23
+ end
24
+
25
+ @actor
26
+ end
27
+
28
+ private
29
+
30
+ def find_actor
31
+ @actor = Masks::Rails::Actor.find(params[:actor])
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ module Manage
6
+ class ActorsController < BaseController
7
+ def index
8
+ @pagy, @actors = pagy(Masks::Rails::Actor.all)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # @visibility private
5
+ module Manage
6
+ class BaseController < ApplicationController
7
+ # require_mask type: :manage
8
+
9
+ layout "masks/manage"
10
+ end
11
+ end
12
+ end