command_tower 0.3.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 (112) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/command_tower/admin_controller.rb +104 -0
  6. data/app/controllers/command_tower/application_controller.rb +81 -0
  7. data/app/controllers/command_tower/auth/plain_text_controller.rb +132 -0
  8. data/app/controllers/command_tower/inbox/message_blast_controller.rb +89 -0
  9. data/app/controllers/command_tower/inbox/message_controller.rb +79 -0
  10. data/app/controllers/command_tower/user_controller.rb +49 -0
  11. data/app/controllers/command_tower/username_controller.rb +26 -0
  12. data/app/helpers/command_tower/application_helper.rb +4 -0
  13. data/app/helpers/command_tower/schema_helper.rb +29 -0
  14. data/app/jobs/command_tower/application_job.rb +4 -0
  15. data/app/mailers/command_tower/application_mailer.rb +8 -0
  16. data/app/mailers/command_tower/email_verification_mailer.rb +12 -0
  17. data/app/models/command_tower/application_record.rb +45 -0
  18. data/app/models/message.rb +30 -0
  19. data/app/models/message_blast.rb +27 -0
  20. data/app/models/user.rb +61 -0
  21. data/app/models/user_secret.rb +72 -0
  22. data/app/services/command_tower/README.md +49 -0
  23. data/app/services/command_tower/argument_validation/README.md +192 -0
  24. data/app/services/command_tower/argument_validation/class_methods.rb +178 -0
  25. data/app/services/command_tower/argument_validation/instance_methods.rb +148 -0
  26. data/app/services/command_tower/argument_validation.rb +11 -0
  27. data/app/services/command_tower/authorize/validate.rb +49 -0
  28. data/app/services/command_tower/inbox_service/blast/delete.rb +23 -0
  29. data/app/services/command_tower/inbox_service/blast/metadata.rb +26 -0
  30. data/app/services/command_tower/inbox_service/blast/new_user_blaster.rb +24 -0
  31. data/app/services/command_tower/inbox_service/blast/retrieve.rb +30 -0
  32. data/app/services/command_tower/inbox_service/blast/upsert.rb +67 -0
  33. data/app/services/command_tower/inbox_service/message/metadata.rb +35 -0
  34. data/app/services/command_tower/inbox_service/message/modify.rb +44 -0
  35. data/app/services/command_tower/inbox_service/message/retrieve.rb +36 -0
  36. data/app/services/command_tower/inbox_service/message/send.rb +33 -0
  37. data/app/services/command_tower/jwt/authenticate_user.rb +86 -0
  38. data/app/services/command_tower/jwt/decode.rb +21 -0
  39. data/app/services/command_tower/jwt/encode.rb +15 -0
  40. data/app/services/command_tower/jwt/login_create.rb +21 -0
  41. data/app/services/command_tower/jwt/time_delay_token.rb +17 -0
  42. data/app/services/command_tower/login_strategy/plain_text/create.rb +43 -0
  43. data/app/services/command_tower/login_strategy/plain_text/email_verification/generate.rb +29 -0
  44. data/app/services/command_tower/login_strategy/plain_text/email_verification/required.rb +20 -0
  45. data/app/services/command_tower/login_strategy/plain_text/email_verification/send.rb +23 -0
  46. data/app/services/command_tower/login_strategy/plain_text/email_verification/verify.rb +24 -0
  47. data/app/services/command_tower/login_strategy/plain_text/login.rb +50 -0
  48. data/app/services/command_tower/secrets/cleanse.rb +14 -0
  49. data/app/services/command_tower/secrets/generate.rb +62 -0
  50. data/app/services/command_tower/secrets/verify.rb +27 -0
  51. data/app/services/command_tower/secrets.rb +15 -0
  52. data/app/services/command_tower/service_base.rb +89 -0
  53. data/app/services/command_tower/service_logging.rb +41 -0
  54. data/app/services/command_tower/user_attributes/modify.rb +68 -0
  55. data/app/services/command_tower/user_attributes/roles.rb +27 -0
  56. data/app/services/command_tower/username/available.rb +64 -0
  57. data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +26 -0
  58. data/config/routes.rb +55 -0
  59. data/db/migrate/20241117043720_create_command_tower_users.rb +42 -0
  60. data/db/migrate/20241204065708_create_command_tower_user_secrets.rb +16 -0
  61. data/db/migrate/20250223023306_create_command_tower_messages.rb +12 -0
  62. data/db/migrate/20250223023313_create_command_tower_message_blasts.rb +14 -0
  63. data/lib/command_tower/authorization/default.yml +42 -0
  64. data/lib/command_tower/authorization/entity.rb +101 -0
  65. data/lib/command_tower/authorization/role.rb +101 -0
  66. data/lib/command_tower/authorization.rb +85 -0
  67. data/lib/command_tower/configuration/admin/config.rb +18 -0
  68. data/lib/command_tower/configuration/application/config.rb +40 -0
  69. data/lib/command_tower/configuration/authorization/config.rb +24 -0
  70. data/lib/command_tower/configuration/base.rb +11 -0
  71. data/lib/command_tower/configuration/config.rb +77 -0
  72. data/lib/command_tower/configuration/email/config.rb +87 -0
  73. data/lib/command_tower/configuration/jwt/config.rb +22 -0
  74. data/lib/command_tower/configuration/login/config.rb +18 -0
  75. data/lib/command_tower/configuration/login/strategy/plain_text/config.rb +57 -0
  76. data/lib/command_tower/configuration/login/strategy/plain_text/email_verify.rb +50 -0
  77. data/lib/command_tower/configuration/login/strategy/plain_text/lockable.rb +27 -0
  78. data/lib/command_tower/configuration/otp/config.rb +54 -0
  79. data/lib/command_tower/configuration/user/config.rb +56 -0
  80. data/lib/command_tower/configuration/username/check.rb +31 -0
  81. data/lib/command_tower/configuration/username/config.rb +41 -0
  82. data/lib/command_tower/engine.rb +53 -0
  83. data/lib/command_tower/error.rb +5 -0
  84. data/lib/command_tower/schema/admin/users.rb +15 -0
  85. data/lib/command_tower/schema/error/base.rb +15 -0
  86. data/lib/command_tower/schema/error/invalid_argument.rb +15 -0
  87. data/lib/command_tower/schema/error/invalid_argument_response.rb +17 -0
  88. data/lib/command_tower/schema/inbox/blast_request.rb +15 -0
  89. data/lib/command_tower/schema/inbox/blast_response.rb +16 -0
  90. data/lib/command_tower/schema/inbox/message_blast_entity.rb +16 -0
  91. data/lib/command_tower/schema/inbox/message_blast_metadata.rb +16 -0
  92. data/lib/command_tower/schema/inbox/message_entity.rb +14 -0
  93. data/lib/command_tower/schema/inbox/metadata.rb +18 -0
  94. data/lib/command_tower/schema/inbox/modified.rb +13 -0
  95. data/lib/command_tower/schema/page.rb +14 -0
  96. data/lib/command_tower/schema/plain_text/create_user_request.rb +18 -0
  97. data/lib/command_tower/schema/plain_text/create_user_response.rb +17 -0
  98. data/lib/command_tower/schema/plain_text/email_verify_request.rb +11 -0
  99. data/lib/command_tower/schema/plain_text/email_verify_response.rb +11 -0
  100. data/lib/command_tower/schema/plain_text/email_verify_send_request.rb +9 -0
  101. data/lib/command_tower/schema/plain_text/email_verify_send_response.rb +11 -0
  102. data/lib/command_tower/schema/plain_text/login_request.rb +15 -0
  103. data/lib/command_tower/schema/plain_text/login_response.rb +13 -0
  104. data/lib/command_tower/schema/user.rb +28 -0
  105. data/lib/command_tower/schema.rb +38 -0
  106. data/lib/command_tower/spec_helper.rb +19 -0
  107. data/lib/command_tower/version.rb +5 -0
  108. data/lib/command_tower.rb +33 -0
  109. data/lib/generators/api_engine_base/configure/USAGE +8 -0
  110. data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
  111. data/lib/tasks/auto_annotate_models.rake +60 -0
  112. metadata +255 -0
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Message
6
+ class Send < CommandTower::ServiceBase
7
+ validate :user, is_a: User, required: true
8
+ validate :text, is_a: String, required: true
9
+ validate :title, is_a: String, required: true
10
+ validate :message_blast, is_a: ::MessageBlast, required: false
11
+ validate :pushed, is_one: [true, false], default: false
12
+
13
+ def call
14
+ message = create_message!
15
+ context.message = message
16
+ end
17
+
18
+ def push_notification!
19
+ # TODO: Push notifications
20
+ end
21
+
22
+ def create_message!
23
+ ::Message.create!(
24
+ user:,
25
+ text: ,
26
+ title: ,
27
+ message_blast:,
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Jwt
4
+ class AuthenticateUser < CommandTower::ServiceBase
5
+
6
+ validate :token, is_a: String, required: true, sensitive: true
7
+ validate :bypass_email_validation, is_one: [true, false], default: false
8
+ validate :with_reset, is_one: [true, false], default: false
9
+
10
+ def call
11
+ result = Decode.(token:)
12
+
13
+ if result.failure?
14
+ context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
15
+ end
16
+ payload = result.payload
17
+
18
+ expires_at = validate_generated_at!(generated_at: payload[:generated_at])
19
+
20
+ user = User.find(payload[:user_id]) rescue nil
21
+ if user.nil?
22
+ log_warn("user_id [#{payload[:user_id]}] was not found. Cannot Continue")
23
+ context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
24
+ end
25
+
26
+ if user.verifier_token == payload[:verifier_token]
27
+ context.user = user
28
+ else
29
+ context.fail!(msg: "Unauthorized Access. Token is no longer valid")
30
+ end
31
+
32
+ email_validation_required!(user:)
33
+
34
+ if with_reset
35
+ context.generated_token = CommandTower::Jwt::LoginCreate.(user:).token
36
+ expires_at = CommandTower.config.jwt.ttl.from_now.to_time
37
+ end
38
+
39
+ context.expires_at = expires_at.to_s
40
+ end
41
+
42
+ def validate_generated_at!(generated_at:)
43
+ if generated_at.nil?
44
+ log_warn("generated_at payload is missing from the JWT token. Cannot continue")
45
+ context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
46
+ end
47
+
48
+ expires_time = begin
49
+ time = Time.at(generated_at)
50
+ time + CommandTower.config.jwt.ttl
51
+ rescue
52
+ nil
53
+ end
54
+
55
+ if expires_time.nil?
56
+ log_warn("generated_at payload cannot be parsed. Cannot continue")
57
+ context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
58
+ end
59
+
60
+ if expires_time < Time.now
61
+ log_warn("generated_at is no longer valid. Must request new token")
62
+ context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
63
+ end
64
+
65
+ expires_time
66
+ end
67
+
68
+ def email_validation_required!(user:)
69
+ return unless CommandTower.config.login.plain_text.email_verify?
70
+
71
+ if bypass_email_validation
72
+ log_info("Bypassing email validation without checking if user should be able to continue")
73
+ return
74
+ end
75
+
76
+ return if user.email_validated
77
+
78
+ log_info("User's email is not yet validated.")
79
+ result = CommandTower::LoginStrategy::PlainText::EmailVerification::Required.(user:)
80
+
81
+ if result.required
82
+ context.fail!(msg: "User's Email must be validated before they can continue")
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+
5
+ module CommandTower::Jwt
6
+ class Decode < CommandTower::ServiceBase
7
+
8
+ validate :token, is_a: String, required: true, sensitive: true
9
+
10
+ def call
11
+ data = JWT.decode(token, CommandTower.config.jwt.hmac_secret, true, { algorithm: "HS256" })
12
+
13
+ context.payload = data[0].with_indifferent_access
14
+ context.headers = data[1].with_indifferent_access
15
+ rescue JWT::DecodeError => e
16
+ log_error(e)
17
+
18
+ context.fail!(msg: "Invalid Token")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+
5
+ module CommandTower::Jwt
6
+ class Encode < CommandTower::ServiceBase
7
+
8
+ validate :payload, is_a: Hash, required: true
9
+ validate :header, is_a: Hash, required: false
10
+
11
+ def call
12
+ context.token = JWT.encode(payload, CommandTower.config.jwt.hmac_secret, "HS256", header || {})
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Jwt
4
+ class LoginCreate < CommandTower::ServiceBase
5
+ on_argument_validation :fail_early
6
+
7
+ validate :user, is_a: User, required: true
8
+
9
+ def call
10
+ context.token = Encode.(payload:).token
11
+ end
12
+
13
+ def payload
14
+ {
15
+ generated_at: Time.now.to_i,
16
+ user_id: user.id,
17
+ verifier_token: user.retreive_verifier_token!,
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Jwt
4
+ class TimeDelayToken < CommandTower::ServiceBase
5
+ on_argument_validation :fail_early
6
+
7
+ validate :expires_in, is_a: ActiveSupport::Duration, required: true
8
+
9
+ def call
10
+ context.token = Encode.(payload:).token
11
+ end
12
+
13
+ def payload
14
+ { expires_in: expires_in }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::LoginStrategy::PlainText
4
+ class Create < CommandTower::ServiceBase
5
+ on_argument_validation :fail_early
6
+
7
+ EASY_GETTER = CommandTower.config.login.plain_text
8
+
9
+ validate :first_name, is_a: String, required: true
10
+ validate :last_name, is_a: String, required: true
11
+ validate :username, is_a: String, required: true
12
+ validate :email, length: true, lt: EASY_GETTER.email_length_max, gt: EASY_GETTER.email_length_min, is_a: String, required: true, sensitive: true
13
+ validate :password, length: true, lt: EASY_GETTER.password_length_max, gt: EASY_GETTER.password_length_min, is_a: String, required: true, sensitive: true
14
+ validate :password_confirmation, is_a: String, required: true, sensitive: true
15
+
16
+ def call
17
+ unless email =~ URI::MailTo::EMAIL_REGEXP
18
+ inline_argument_failure!(errors: { email: "Invalid email address" })
19
+ end
20
+
21
+ username_validity = CommandTower::Username::Available.(username:)
22
+ if !username_validity.valid
23
+ inline_argument_failure!(errors: { username: "Username is invalid. #{CommandTower.config.username.username_failure_message}" })
24
+ end
25
+
26
+ user = User.new(
27
+ first_name:,
28
+ last_name:,
29
+ username:,
30
+ email:,
31
+ password:,
32
+ password_confirmation:,
33
+ )
34
+
35
+ if user.save
36
+ context.user = user
37
+ CommandTower::InboxService::Blast::NewUserBlaster.(user:)
38
+ else
39
+ inline_argument_failure!(errors: user.errors)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::LoginStrategy::PlainText::EmailVerification
4
+ class Generate < CommandTower::ServiceBase
5
+ validate :user, is_a: User, required: true
6
+
7
+ def call
8
+ result = CommandTower::Secrets::Generate.(
9
+ user:,
10
+ secret_length: email_verify.verify_code_length,
11
+ reason: CommandTower::Secrets::EMAIL_VERIFICIATION,
12
+ use_count_max: 1,
13
+ death_time: email_verify.verify_code_link_valid_for,
14
+ type: CommandTower::Secrets::NUMERIC,
15
+ cleanse: true,
16
+ )
17
+
18
+ if result.failure?
19
+ context.fail!(msg: "Secret Generation is not available at this time")
20
+ end
21
+
22
+ context.secret = result.secret
23
+ end
24
+
25
+ def email_verify
26
+ CommandTower.config.login.plain_text.email_verify
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::LoginStrategy::PlainText::EmailVerification
4
+ class Required < CommandTower::ServiceBase
5
+ validate :user, is_a: User, required: true
6
+
7
+ def call
8
+ context.reqired_after_time = reqired_after_time
9
+ context.required = Time.now > reqired_after_time
10
+ end
11
+
12
+ def reqired_after_time
13
+ user.created_at + email_verify.verify_email_required_within
14
+ end
15
+
16
+ def email_verify
17
+ CommandTower.config.login.plain_text.email_verify
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::LoginStrategy::PlainText::EmailVerification
4
+ class Send < CommandTower::ServiceBase
5
+ on_argument_validation :fail_early
6
+
7
+ validate :user, is_a: User, required: true
8
+
9
+ def call
10
+ result = Generate.(user:)
11
+ if result.failure?
12
+ context.fail!(msg: result.msg)
13
+ end
14
+
15
+ begin
16
+ CommandTower::EmailVerificationMailer.verify_email(user.email, user, result.secret).deliver
17
+ rescue StandardError => e
18
+ log_error("Failed to send message to [#{user.id}]: #{e.message}")
19
+ context.fail!(msg: "Unable to send email. Please try again later", status: 500)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::LoginStrategy::PlainText::EmailVerification
4
+ class Verify < CommandTower::ServiceBase
5
+ on_argument_validation :fail_early
6
+
7
+ validate :user, is_a: User, required: true
8
+ validate :code, is_a: String, required: true
9
+
10
+ def call
11
+ result = CommandTower::Secrets::Verify.(secret: code, reason: CommandTower::Secrets::EMAIL_VERIFICIATION)
12
+ if result.failure?
13
+ inline_argument_failure!(errors: { code: "Incorrect verification code provided" })
14
+ end
15
+
16
+ if result.user != user
17
+ log_warn("Yikes! The logged in user does not match the correct code. Kick them back out and do not verify")
18
+ inline_argument_failure!(errors: { code: "Incorrect verification code provided" })
19
+ end
20
+
21
+ user.update(email_validated: true)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::LoginStrategy::PlainText
4
+ class Login < CommandTower::ServiceBase
5
+ on_argument_validation :fail_early
6
+
7
+ one_of(:login_key, required: true) do
8
+ validate :username, is_a: String, sensitive: true
9
+ validate :email, is_a: String, sensitive: true
10
+ end
11
+ validate :password, is_a: String, required: true, sensitive: true
12
+
13
+ def call
14
+ if user.nil?
15
+ log_warn("Login attempted with [#{login_key_key}] => [#{login_key}]. Resource not found")
16
+ credential_mismatch!
17
+ end
18
+
19
+ if user.authenticate(password)
20
+ user.successful_login += 1
21
+ user.password_consecutive_fail = 0
22
+ user.save
23
+ else
24
+ user.password_consecutive_fail += 1
25
+ user.save
26
+ log_warn("Valid #{login_key_key}. Incorrect password. Consecutive Password failures: #{user.password_consecutive_fail}")
27
+ credential_mismatch!
28
+ end
29
+
30
+ context.user = user
31
+
32
+ result = CommandTower::Jwt::LoginCreate.(user:)
33
+ if result.failure?
34
+ context.fail!(msg: "Failed to generate Authorization. Please Try again")
35
+ return
36
+ end
37
+
38
+ context.token = result.token
39
+ end
40
+
41
+ def credential_mismatch!
42
+ msg = "Unauthorized Access. Incorrect Credentials"
43
+ inline_argument_failure!(errors: { login_key_key => msg, password: msg })
44
+ end
45
+
46
+ def user
47
+ @user ||= User.where(login_key_key => login_key).first
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Secrets
4
+ class Cleanse < CommandTower::ServiceBase
5
+ validate :user, is_a: User, required: true
6
+ validate :reason, is_one: ALLOWED_SECRET_REASONS, required: true
7
+
8
+ def call
9
+ secrets = UserSecret.where(user:, reason:)
10
+ count = secrets.delete_all
11
+ log_info("Cleansed #{count} #{reason} secret(s) from the store for user [#{user.id}]")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Secrets
4
+ class Generate < CommandTower::ServiceBase
5
+ MAX_RETRY = 10
6
+
7
+ validate :user, is_a: User, required: true
8
+ validate :secret_length, is_a: Integer, gte: 4, lte: 64, required: true
9
+ validate :reason, is_one: ALLOWED_SECRET_REASONS, required: true
10
+ validate :type, is_one: ALLOWED_SECRET_TYPES, default: DEFAULT_SECRET_TYPE
11
+ validate :extra, is_a: String, length: true, lt: 256
12
+ validate :cleanse, is_one: [true, false], default: false
13
+ at_least_one(:death, required: true) do
14
+ validate :death_time, is_a: ActiveSupport::Duration, gte: 10.seconds
15
+ validate :use_count_max, is_a: Integer, gte: 1
16
+ end
17
+
18
+ def call
19
+ if cleanse && @attempts.nil?
20
+ # if this fails ... so be it
21
+ Cleanse.(user:, reason:)
22
+ end
23
+
24
+ @attempts ||= 1
25
+ record = UserSecret.create!(**db_params)
26
+
27
+ context.record = record
28
+ context.secret = record.secret
29
+ rescue ActiveRecord::RecordNotUnique => e
30
+ if @attempts < MAX_RETRY
31
+ @attempts += 1
32
+ log_warn("Duplicate Secret was generated. Attempting to retry: #{@attempts} of #{MAX_RETRY}")
33
+ retry
34
+ else
35
+ log_error("Duplicate Secret was generated. Exhausted Max attempts of #{MAX_RETRY}.")
36
+ context.fail!(msg: "Failed to generate Secret. Cannot Continue")
37
+ end
38
+ end
39
+
40
+ def db_params
41
+ {
42
+ death_time: death_time&.from_now,
43
+ use_count_max:,
44
+ extra:,
45
+ reason:,
46
+ secret: generate_secret,
47
+ user:,
48
+ }.compact
49
+ end
50
+
51
+ def generate_secret
52
+ case type
53
+ when :numeric
54
+ secret_length.times.map { SecureRandom.rand(0...10) }.join
55
+ when :alphanumeric, :hex
56
+ SecureRandom.public_send(type, secret_length)
57
+ when :uuid
58
+ SecureRandom.public_send(type)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Secrets
4
+ class Verify < CommandTower::ServiceBase
5
+ validate :secret, is_a: String, required: true, sensitive: true
6
+ validate :reason, is_one: ALLOWED_SECRET_REASONS, required: true
7
+ validate :access_count, is_one: [true, false], default: false
8
+
9
+ def call
10
+ record = UserSecret.find_record(secret:, reason:, access_count:)
11
+
12
+ if record[:found] == false
13
+ context.fail!(record:, msg: "Secret not found")
14
+ end
15
+
16
+ if record[:valid] == false
17
+ if CommandTower.config.delete_secret_after_invalid
18
+ record[:record].destroy
19
+ end
20
+
21
+ context.fail!(record:, msg: "Secret is invalid. #{record[:record].invalid_reason.join(" ")}")
22
+ end
23
+
24
+ context.user = record[:user]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Secrets
4
+ ALLOWED_SECRET_REASONS = [
5
+ EMAIL_VERIFICIATION = :email_verification,
6
+ SSO = :sso,
7
+ ]
8
+
9
+ ALLOWED_SECRET_TYPES = [
10
+ DEFAULT_SECRET_TYPE = ALPHANUMERIC = :alphanumeric,
11
+ HEX = :hex,
12
+ NUMERIC = :numeric,
13
+ UUID = :uuid,
14
+ ]
15
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactor"
4
+
5
+ class CommandTower::ServiceBase
6
+ class ServiceBaseError < CommandTower::Error; end;
7
+ class ValidationError < ServiceBaseError; end;
8
+ class ConfigurationError < ServiceBaseError; end;
9
+
10
+ class NameConflictError < CommandTower::Error; end
11
+ class DefaultValueError < CommandTower::Error; end
12
+ class OneOfError < CommandTower::Error; end
13
+ class NestedOneOfError < CommandTower::Error; end
14
+ class ArgumentValidationError < CommandTower::Error; end
15
+
16
+ class KeyValidationError < CommandTower::ServiceBase::ValidationError; end
17
+ class CompositionValidationError < CommandTower::ServiceBase::ValidationError; end
18
+
19
+ include Interactor
20
+ include CommandTower::ServiceLogging
21
+ include CommandTower::ArgumentValidation
22
+
23
+ ON_ARGUMENT_VALIDATION = [
24
+ DEFAULT_VALIDATION = :raise,
25
+ :fail_early,
26
+ :log,
27
+ ]
28
+
29
+ def self.inherited(subclass)
30
+ # Add the base logging to the subclass.
31
+ # Since this is done at inheritance time it should always be the first and last hook to run.
32
+ subclass.around(:service_base_logging)
33
+ subclass.around(:internal_validate)
34
+ subclass.after(:sanitize_params)
35
+ end
36
+
37
+ def validate!
38
+ # overload from child
39
+ end
40
+
41
+ def internal_validate(interactor)
42
+ # call validate that is overridden from child
43
+ begin
44
+ validate! # custom validations defined on the child class
45
+ run_validations! # ArgumentValidation's based on defined settings on child
46
+ rescue StandardError => e
47
+ log_error("Error during validation. #{e.message}")
48
+ raise
49
+ end
50
+
51
+ # call interactor
52
+ interactor.call
53
+ end
54
+
55
+ def service_base_logging(interactor)
56
+ beginning_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
57
+
58
+ # Pre processing stats
59
+ log_info("Start")
60
+
61
+ # Run the job!
62
+ interactor.call
63
+
64
+ # Set status for use in ensure block
65
+ status = :complete
66
+
67
+ # Capture Interactor::Failure for logging purposes, then reraise
68
+ rescue ::Interactor::Failure
69
+ # set status for use in ensure block
70
+ status = :failure
71
+
72
+ # Re-raise to let the core Interactor handle this
73
+ raise
74
+ # Capture exception explicitly for logging purposes, then reraise
75
+ rescue ::Exception => e
76
+ # set status for use in ensure block
77
+ status = :error
78
+
79
+ # Log error
80
+ log_error("Error #{e.class.name}")
81
+
82
+ raise
83
+ ensure
84
+ # Always log how long it took along with a status
85
+ finished_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
86
+ elapsed = ((finished_time - beginning_time) * 1000).round(2)
87
+ log_info("Finished with [#{status}]...elapsed #{elapsed}ms")
88
+ end
89
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::ServiceLogging
4
+ def log_info(msg)
5
+ log(level: :info, msg:)
6
+ end
7
+
8
+ def log_warn(msg)
9
+ log(level: :warn, msg:)
10
+ end
11
+
12
+ def log_error(msg)
13
+ log(level: :error, msg:)
14
+ end
15
+
16
+ def log(level:, msg:)
17
+ logger.public_send(level, aletered_message(msg))
18
+ rescue StandardError
19
+ Rails.logger.public_send(level, aletered_message(msg))
20
+ end
21
+
22
+ def aletered_message(msg)
23
+ "#{log_prefix}: #{msg}"
24
+ end
25
+
26
+ def logger
27
+ defined?(context) ? context.logger : Rails.logger
28
+ end
29
+
30
+ def log_prefix
31
+ "[#{class_name}-#{service_id}]"
32
+ end
33
+
34
+ def class_name
35
+ self.class.name
36
+ end
37
+
38
+ def service_id
39
+ @service_id ||= SecureRandom.alphanumeric(10)
40
+ end
41
+ end