api_engine_base 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +32 -0
- data/app/controllers/api_engine_base/application_controller.rb +47 -0
- data/app/controllers/api_engine_base/auth/plain_text_controller.rb +132 -0
- data/app/controllers/api_engine_base/username_controller.rb +26 -0
- data/app/controllers/concerns/api_engine_base/schematizable.rb +5 -0
- data/app/helpers/api_engine_base/application_helper.rb +4 -0
- data/app/helpers/api_engine_base/schema_helper.rb +29 -0
- data/app/jobs/api_engine_base/application_job.rb +4 -0
- data/app/mailers/api_engine_base/application_mailer.rb +8 -0
- data/app/mailers/api_engine_base/email_verification_mailer.rb +12 -0
- data/app/models/api_engine_base/application_record.rb +7 -0
- data/app/models/user.rb +50 -0
- data/app/models/user_secret.rb +72 -0
- data/app/services/api_engine_base/argument_validation/class_methods.rb +179 -0
- data/app/services/api_engine_base/argument_validation/instance_methods.rb +136 -0
- data/app/services/api_engine_base/argument_validation.rb +11 -0
- data/app/services/api_engine_base/jwt/authenticate_user.rb +71 -0
- data/app/services/api_engine_base/jwt/decode.rb +21 -0
- data/app/services/api_engine_base/jwt/encode.rb +15 -0
- data/app/services/api_engine_base/jwt/login_create.rb +21 -0
- data/app/services/api_engine_base/jwt/time_delay_token.rb +17 -0
- data/app/services/api_engine_base/login_strategy/plain_text/create.rb +42 -0
- data/app/services/api_engine_base/login_strategy/plain_text/email_verification/generate.rb +29 -0
- data/app/services/api_engine_base/login_strategy/plain_text/email_verification/required.rb +20 -0
- data/app/services/api_engine_base/login_strategy/plain_text/email_verification/send.rb +23 -0
- data/app/services/api_engine_base/login_strategy/plain_text/email_verification/verify.rb +24 -0
- data/app/services/api_engine_base/login_strategy/plain_text/login.rb +50 -0
- data/app/services/api_engine_base/secrets/cleanse.rb +14 -0
- data/app/services/api_engine_base/secrets/generate.rb +62 -0
- data/app/services/api_engine_base/secrets/verify.rb +27 -0
- data/app/services/api_engine_base/secrets.rb +15 -0
- data/app/services/api_engine_base/service_base.rb +90 -0
- data/app/services/api_engine_base/service_logging.rb +41 -0
- data/app/services/api_engine_base/username/available.rb +64 -0
- data/app/views/api_engine_base/email_verification_mailer/verify_email.html.erb +26 -0
- data/config/routes.rb +23 -0
- data/db/migrate/20241117043720_create_api_engine_base_users.rb +33 -0
- data/db/migrate/20241204065708_create_api_engine_base_user_secrets.rb +16 -0
- data/lib/api_engine_base/configuration/application/config.rb +40 -0
- data/lib/api_engine_base/configuration/base.rb +11 -0
- data/lib/api_engine_base/configuration/config.rb +59 -0
- data/lib/api_engine_base/configuration/email/config.rb +87 -0
- data/lib/api_engine_base/configuration/jwt/config.rb +22 -0
- data/lib/api_engine_base/configuration/login/config.rb +18 -0
- data/lib/api_engine_base/configuration/login/strategy/plain_text/config.rb +57 -0
- data/lib/api_engine_base/configuration/login/strategy/plain_text/email_verify.rb +50 -0
- data/lib/api_engine_base/configuration/login/strategy/plain_text/lockable.rb +27 -0
- data/lib/api_engine_base/configuration/otp/config.rb +54 -0
- data/lib/api_engine_base/configuration/username/check.rb +31 -0
- data/lib/api_engine_base/configuration/username/config.rb +41 -0
- data/lib/api_engine_base/engine.rb +21 -0
- data/lib/api_engine_base/schema/error/base.rb +15 -0
- data/lib/api_engine_base/schema/error/invalid_argument.rb +15 -0
- data/lib/api_engine_base/schema/error/invalid_argument_response.rb +17 -0
- data/lib/api_engine_base/schema/plain_text/create_user_request.rb +18 -0
- data/lib/api_engine_base/schema/plain_text/create_user_response.rb +17 -0
- data/lib/api_engine_base/schema/plain_text/email_verify_request.rb +11 -0
- data/lib/api_engine_base/schema/plain_text/email_verify_response.rb +11 -0
- data/lib/api_engine_base/schema/plain_text/email_verify_send_request.rb +9 -0
- data/lib/api_engine_base/schema/plain_text/email_verify_send_response.rb +11 -0
- data/lib/api_engine_base/schema/plain_text/login_request.rb +15 -0
- data/lib/api_engine_base/schema/plain_text/login_response.rb +13 -0
- data/lib/api_engine_base/schema.rb +25 -0
- data/lib/api_engine_base/spec_helper.rb +18 -0
- data/lib/api_engine_base/version.rb +5 -0
- data/lib/api_engine_base.rb +33 -0
- data/lib/generators/api_engine_base/configure/USAGE +8 -0
- data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
- data/lib/tasks/auto_annotate_models.rake +60 -0
- metadata +216 -0
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::ArgumentValidation
|
4
|
+
module InstanceMethods
|
5
|
+
def run_validations!
|
6
|
+
context.valid_arguments = true
|
7
|
+
|
8
|
+
validate_param!
|
9
|
+
validate_compositions!
|
10
|
+
continue_with_logical_code!
|
11
|
+
end
|
12
|
+
|
13
|
+
def continue_with_logical_code!
|
14
|
+
return if @context_validation_failures.nil?
|
15
|
+
|
16
|
+
invalid_argument_keys = @context_validation_failures.keys
|
17
|
+
msg = @context_validation_failures.map { |_k, obj| obj[:msg] }.join(", ")
|
18
|
+
context.fail!(msg:, invalid_argument_hash: @context_validation_failures, invalid_argument_keys:, invalid_arguments: true)
|
19
|
+
end
|
20
|
+
|
21
|
+
def inline_argument_failure!(errors:)
|
22
|
+
errors = errors.to_hash
|
23
|
+
invalid_argument_keys = errors.keys
|
24
|
+
invalid_argument_hash = {}
|
25
|
+
human_readable = []
|
26
|
+
|
27
|
+
errors.each do |k, v|
|
28
|
+
error_message = Array(v).join(", ")
|
29
|
+
invalid_argument_hash[k] = { msg: error_message }
|
30
|
+
human_readable << "#{k}: #{error_message}"
|
31
|
+
end
|
32
|
+
|
33
|
+
msg = "Invalid arguments: #{human_readable.join(", ")}"
|
34
|
+
context.fail!(msg:, invalid_argument_hash:, invalid_argument_keys:, invalid_arguments: true)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_param!
|
38
|
+
self.class.validate_params.each do |metadata|
|
39
|
+
value = context.public_send(metadata[:name])
|
40
|
+
use_length = metadata[:length]
|
41
|
+
if metadata[:required] && value.nil?
|
42
|
+
__failed_argument_validation(msg: "Parameter [#{metadata[:name]}] is required but not present", argument: metadata[:name], metadata:)
|
43
|
+
end
|
44
|
+
|
45
|
+
if value.nil? && metadata[:default]
|
46
|
+
context.public_send(:"#{metadata[:name]}=", metadata[:default])
|
47
|
+
next
|
48
|
+
elsif value.nil?
|
49
|
+
next
|
50
|
+
end
|
51
|
+
|
52
|
+
if is_a = metadata[:is_a]
|
53
|
+
if Array(is_a).none? { _1 === value }
|
54
|
+
__failed_argument_validation(msg: "Parameter [#{metadata[:name]}] must be of type #{is_a}. Given #{value.class} [#{value}]", argument: metadata[:name], metadata:)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if metadata[:is_one]
|
59
|
+
if Array(metadata[:is_one]).none? { _1 == value }
|
60
|
+
__failed_argument_validation(msg: "Parameter [#{metadata[:name]}] must be one of #{Array(metadata[:is_one])}. Given #{value}", argument: metadata[:name], metadata:)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
validate_sign!(name: metadata[:name], value:, sign: "lt", validation: metadata[:lte], use_length:, metadata:) { (use_length ? value.length : value) <= _1 }
|
65
|
+
validate_sign!(name: metadata[:name], value:, sign: "lte", validation: metadata[:lt], use_length:, metadata:) { (use_length ? value.length : value) < _1 }
|
66
|
+
validate_sign!(name: metadata[:name], value:, sign: "eq", validation: metadata[:eq], use_length:, metadata:) { (use_length ? value.length : value) == _1 }
|
67
|
+
validate_sign!(name: metadata[:name], value:, sign: "gte", validation: metadata[:gte], use_length:, metadata:) { (use_length ? value.length : value) >= _1 }
|
68
|
+
validate_sign!(name: metadata[:name], value:, sign: "gt", validation: metadata[:gt], use_length:, metadata:) { (use_length ? value.length : value) > _1 }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_compositions!
|
73
|
+
self.class.compositions.each do |type, metadata|
|
74
|
+
value_list = {}
|
75
|
+
|
76
|
+
metadata[:keys].each do |argument|
|
77
|
+
value = context.public_send(argument)
|
78
|
+
next if value.nil?
|
79
|
+
|
80
|
+
value_list[argument] = value
|
81
|
+
end
|
82
|
+
|
83
|
+
composition_result = metadata[:validation_proc].(value_list.count, value_list.keys)
|
84
|
+
|
85
|
+
next if value_list.count == 0 && !metadata[:required]
|
86
|
+
|
87
|
+
if !composition_result[:is_valid]
|
88
|
+
composition_result[:message]
|
89
|
+
context.client_composite_error = composition_result[:requirement]
|
90
|
+
msg = "Composite Key failure for #{type} [#{metadata[:name]}]. #{composition_result[:message]}. Provided values for the following keys: #{value_list.keys}. Available keys #{metadata[:keys]}"
|
91
|
+
__failed_argument_validation(msg:, argument: metadata[:name], metadata: ,error: ApiEngineBase::ServiceBase::CompositionValidationError)
|
92
|
+
next
|
93
|
+
end
|
94
|
+
|
95
|
+
if metadata[:delegation]
|
96
|
+
context.public_send("#{metadata[:name]}=", value_list.first[1])
|
97
|
+
context.public_send("#{metadata[:name]}_key=", value_list.first[0])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def validate_sign!(name:, value:, sign:, validation:, use_length:, metadata:)
|
103
|
+
return if validation.nil?
|
104
|
+
return if yield(validation)
|
105
|
+
|
106
|
+
__failed_argument_validation(metadata:, msg: "Parameter [#{name}]#{ " lengths" if use_length} must be #{sign} to #{validation}. Given #{value}", argument: name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def __failed_argument_validation(msg:, argument:, metadata:, error: ApiEngineBase::ServiceBase::ArgumentValidationError)
|
110
|
+
case self.class.on_argument_validation_assigned
|
111
|
+
when :raise
|
112
|
+
raise error, msg
|
113
|
+
when :fail_early
|
114
|
+
@context_validation_failures ||= {}
|
115
|
+
@context_validation_failures[argument] = {
|
116
|
+
msg: msg,
|
117
|
+
required: metadata[:requirement],
|
118
|
+
is_a: metadata[:is_a],
|
119
|
+
}
|
120
|
+
# When gracefully failing, it will find all failures first before setting appropriate
|
121
|
+
# context variables -- Check out continue_with_logical_code!
|
122
|
+
else
|
123
|
+
context.invalid_arguments = true
|
124
|
+
log_warn(msg)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def sanitize_params
|
129
|
+
self.class.sensitive_params.each do |param|
|
130
|
+
next if context.send(param).nil?
|
131
|
+
|
132
|
+
context.send("#{param}=","[FILTERED]")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::ArgumentValidation
|
4
|
+
class NameConflictError < ApiEngineBase::ServiceBase::ConfigurationError; end
|
5
|
+
class NestedDuplicateTypeError < ApiEngineBase::ServiceBase::ConfigurationError; end
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ApiEngineBase::ArgumentValidation::ClassMethods)
|
9
|
+
base.include(ApiEngineBase::ArgumentValidation::InstanceMethods)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::Jwt
|
4
|
+
class AuthenticateUser < ApiEngineBase::ServiceBase
|
5
|
+
|
6
|
+
validate :token, is_a: String, required: true, sensitive: true
|
7
|
+
validate :bypass_email_validation, is_one: [true, false], default: false
|
8
|
+
|
9
|
+
def call
|
10
|
+
result = Decode.(token:)
|
11
|
+
|
12
|
+
if result.failure?
|
13
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
14
|
+
end
|
15
|
+
payload = result.payload
|
16
|
+
|
17
|
+
validate_expires_at!(expires_at: payload[:expires_at])
|
18
|
+
|
19
|
+
user = User.find(payload[:user_id]) rescue nil
|
20
|
+
if user.nil?
|
21
|
+
log_warn("user_id [#{payload[:user_id]}] was not found. Cannot Continue")
|
22
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
23
|
+
end
|
24
|
+
|
25
|
+
if user.verifier_token == payload[:verifier_token]
|
26
|
+
context.user = user
|
27
|
+
else
|
28
|
+
context.fail!(msg: "Unauthorized Access. Token is no longer valid")
|
29
|
+
end
|
30
|
+
|
31
|
+
email_validation_required!(user:)
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_expires_at!(expires_at:)
|
35
|
+
if expires_at.nil?
|
36
|
+
log_warn("expires_at payload is missing from the JWT token. Cannot continue")
|
37
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
38
|
+
end
|
39
|
+
|
40
|
+
expires_time = Time.at(expires_at) rescue nil
|
41
|
+
|
42
|
+
if expires_time.nil?
|
43
|
+
log_warn("expires_at payload cannot be parsed. Cannot continue")
|
44
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
45
|
+
end
|
46
|
+
|
47
|
+
if expires_time < Time.now
|
48
|
+
log_warn("expires_at is no longer valid. Must request new token")
|
49
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def email_validation_required!(user:)
|
54
|
+
return unless ApiEngineBase.config.login.plain_text.email_verify?
|
55
|
+
|
56
|
+
if bypass_email_validation
|
57
|
+
log_info("Bypassing email validation without checking if user should be able to continue")
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
return if user.email_validated
|
62
|
+
|
63
|
+
log_info("User's email is not yet validated.")
|
64
|
+
result = ApiEngineBase::LoginStrategy::PlainText::EmailVerification::Required.(user:)
|
65
|
+
|
66
|
+
if result.required
|
67
|
+
context.fail!(msg: "User's Email must be validated before they can continue")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jwt"
|
4
|
+
|
5
|
+
module ApiEngineBase::Jwt
|
6
|
+
class Decode < ApiEngineBase::ServiceBase
|
7
|
+
|
8
|
+
validate :token, is_a: String, required: true, sensitive: true
|
9
|
+
|
10
|
+
def call
|
11
|
+
data = JWT.decode(token, ApiEngineBase.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 ApiEngineBase::Jwt
|
6
|
+
class Encode < ApiEngineBase::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, ApiEngineBase.config.jwt.hmac_secret, "HS256", header || {})
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::Jwt
|
4
|
+
class LoginCreate < ApiEngineBase::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
|
+
expires_at: ApiEngineBase.config.jwt.ttl.from_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 ApiEngineBase::Jwt
|
4
|
+
class TimeDelayToken < ApiEngineBase::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,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::LoginStrategy::PlainText
|
4
|
+
class Create < ApiEngineBase::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
EASY_GETTER = ApiEngineBase.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 = ApiEngineBase::Username::Available.(username:)
|
22
|
+
if !username_validity.valid
|
23
|
+
inline_argument_failure!(errors: { username: "Username is invalid. #{ApiEngineBase.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
|
+
else
|
38
|
+
inline_argument_failure!(errors: user.errors)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Generate < ApiEngineBase::ServiceBase
|
5
|
+
validate :user, is_a: User, required: true
|
6
|
+
|
7
|
+
def call
|
8
|
+
result = ApiEngineBase::Secrets::Generate.(
|
9
|
+
user:,
|
10
|
+
secret_length: email_verify.verify_code_length,
|
11
|
+
reason: ApiEngineBase::Secrets::EMAIL_VERIFICIATION,
|
12
|
+
use_count_max: 1,
|
13
|
+
death_time: email_verify.verify_code_link_valid_for,
|
14
|
+
type: ApiEngineBase::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
|
+
ApiEngineBase.config.login.plain_text.email_verify
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Required < ApiEngineBase::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
|
+
ApiEngineBase.config.login.plain_text.email_verify
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Send < ApiEngineBase::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
|
+
ApiEngineBase::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 ApiEngineBase::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Verify < ApiEngineBase::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 = ApiEngineBase::Secrets::Verify.(secret: code, reason: ApiEngineBase::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 ApiEngineBase::LoginStrategy::PlainText
|
4
|
+
class Login < ApiEngineBase::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 = ApiEngineBase::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 ApiEngineBase::Secrets
|
4
|
+
class Cleanse < ApiEngineBase::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 ApiEngineBase::Secrets
|
4
|
+
class Generate < ApiEngineBase::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 ApiEngineBase::Secrets
|
4
|
+
class Verify < ApiEngineBase::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 ApiEngineBase.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 ApiEngineBase::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
|