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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 237d043d261233c9e45d4b7466dcacbfb4792636246fa2e211e765ee21b75141
|
4
|
+
data.tar.gz: 55d598e1abaf7a12a253fbc9ea8d010c9a11ac9ca5ea293e2a30f6cb42ffc759
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 53180702bea719474e8f9922858da3c1af7b6c44ce3ad1db78bb5c633c039b9b5bb7a524984a637e41dd16499478f896731f033a8c8beece0441eff76ac40fa9
|
7
|
+
data.tar.gz: '03397ea3efb5e7a68554ae600d4eda0d0f467d2990a14dbb4e1bf2ea2cd13a96e82abd679894e2943c51e097eab095c10434116303c1004404626d84d2805c5a'
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ApiEngineBase
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "api_engine_base"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install api_engine_base
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'ApiEngineBase'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("rails_app/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase
|
4
|
+
class ApplicationController < ActionController::API
|
5
|
+
AUTHORIZATION_HEADER = "AUTHORIZATION"
|
6
|
+
|
7
|
+
###
|
8
|
+
# AUTHORIZATION_HEADER="Bearer: {token value}"
|
9
|
+
def authenticate_user!(bypass_email_validation: false)
|
10
|
+
raw_token = request.headers[AUTHORIZATION_HEADER]
|
11
|
+
if raw_token.nil?
|
12
|
+
status = 401
|
13
|
+
schema = ApiEngineBase::Schema::Error::Base.new(status:, message: "Bearer token missing")
|
14
|
+
render(json: schema.to_h, status:)
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
token = raw_token.split("Bearer:")[1].strip
|
19
|
+
result = ApiEngineBase::Jwt::AuthenticateUser.(token:, bypass_email_validation:)
|
20
|
+
if result.success?
|
21
|
+
@current_user = result.user
|
22
|
+
true
|
23
|
+
else
|
24
|
+
status = 401
|
25
|
+
schema = ApiEngineBase::Schema::Error::Base.new(status:, message: result.msg)
|
26
|
+
render(json: schema.to_h, status:)
|
27
|
+
# Must return false so callbacks know to halt propagation
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def authenticate_user_without_email_verification!
|
33
|
+
authenticate_user!(bypass_email_validation: true)
|
34
|
+
end
|
35
|
+
|
36
|
+
def current_user
|
37
|
+
@current_user ||= nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_to_body
|
41
|
+
# {
|
42
|
+
# token_valid_till:,
|
43
|
+
# needs_email_verification:,
|
44
|
+
# }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module ApiEngineBase
|
2
|
+
module Auth
|
3
|
+
class PlainTextController < ::ApiEngineBase::ApplicationController
|
4
|
+
include ApiEngineBase::SchemaHelper
|
5
|
+
|
6
|
+
before_action :authenticate_user_without_email_verification!, only: [:email_verify_post, :email_verify_resend_post]
|
7
|
+
|
8
|
+
# POST /auth/login
|
9
|
+
# Login to the application and create/set the JWT token
|
10
|
+
def login_post
|
11
|
+
result = ApiEngineBase::LoginStrategy::PlainText::Login.(**login_params)
|
12
|
+
if result.success?
|
13
|
+
schema = ApiEngineBase::Schema::PlainText::LoginResponse.new(
|
14
|
+
token: result.token,
|
15
|
+
header_name: AUTHORIZATION_HEADER,
|
16
|
+
message: "Successfully logged user in"
|
17
|
+
)
|
18
|
+
status = 201
|
19
|
+
schema_succesful!(status:, schema:)
|
20
|
+
else
|
21
|
+
if result.invalid_arguments
|
22
|
+
invalid_arguments!(
|
23
|
+
status: 401,
|
24
|
+
message: result.msg,
|
25
|
+
argument_object: result.invalid_argument_hash,
|
26
|
+
schema: ApiEngineBase::Schema::PlainText::LoginRequest
|
27
|
+
)
|
28
|
+
else
|
29
|
+
json_result = { msg: result.msg }
|
30
|
+
status = 400
|
31
|
+
render(json: schema.to_h, status:)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# POST /auth/create
|
37
|
+
# New PlainText user creation
|
38
|
+
def create_post
|
39
|
+
result = ApiEngineBase::LoginStrategy::PlainText::Create.(**create_params)
|
40
|
+
if result.success?
|
41
|
+
schema = ApiEngineBase::Schema::PlainText::CreateUserResponse.new(
|
42
|
+
full_name: result.user.full_name,
|
43
|
+
first_name: result.first_name,
|
44
|
+
last_name: result.last_name,
|
45
|
+
username: result.username,
|
46
|
+
email: result.email,
|
47
|
+
msg: "Successfully created new User",
|
48
|
+
)
|
49
|
+
status = 201
|
50
|
+
schema_succesful!(status:, schema:)
|
51
|
+
else
|
52
|
+
if result.invalid_arguments
|
53
|
+
invalid_arguments!(
|
54
|
+
status: 400,
|
55
|
+
message: result.msg,
|
56
|
+
argument_object: result.invalid_argument_hash,
|
57
|
+
schema: ApiEngineBase::Schema::PlainText::CreateUserRequest
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# POST /auth/email/verify
|
64
|
+
# Verifies a logged in users email verification code when enabled
|
65
|
+
def email_verify_post
|
66
|
+
if current_user.email_validated
|
67
|
+
schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Email is already verified.")
|
68
|
+
status = 200
|
69
|
+
schema_succesful!(status:, schema:)
|
70
|
+
else
|
71
|
+
result = ApiEngineBase::LoginStrategy::PlainText::EmailVerification::Verify.(user: current_user, code: params[:code])
|
72
|
+
if result.success?
|
73
|
+
schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Successfully verified email")
|
74
|
+
status = 201
|
75
|
+
schema_succesful!(status:, schema:)
|
76
|
+
else
|
77
|
+
if result.invalid_arguments
|
78
|
+
invalid_arguments!(
|
79
|
+
status: result.status || 403,
|
80
|
+
message: result.msg,
|
81
|
+
argument_object: result.invalid_argument_hash,
|
82
|
+
schema: ApiEngineBase::Schema::PlainText::EmailVerifyRequest
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# POST /auth/email/send
|
90
|
+
# Sends a logged in users email verification code
|
91
|
+
def email_verify_resend_post
|
92
|
+
if current_user.email_validated
|
93
|
+
schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Email is already verified. No code required")
|
94
|
+
status = 200
|
95
|
+
schema_succesful!(status:, schema:)
|
96
|
+
else
|
97
|
+
result = ApiEngineBase::LoginStrategy::PlainText::EmailVerification::Send.(user: current_user)
|
98
|
+
if result.success?
|
99
|
+
schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Successfully sent Email verification code")
|
100
|
+
status = 201
|
101
|
+
schema_succesful!(status:, schema:)
|
102
|
+
else
|
103
|
+
schema = ApiEngineBase::Schema::Error::Base.new(status:, message: result.msg)
|
104
|
+
status = result.status || 401
|
105
|
+
render(json: schema.to_h, status:)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def login_params
|
113
|
+
{
|
114
|
+
username: params[:username],
|
115
|
+
email: params[:email],
|
116
|
+
password: params[:password],
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_params
|
121
|
+
{
|
122
|
+
first_name: params[:first_name],
|
123
|
+
last_name: params[:last_name],
|
124
|
+
username: params[:username],
|
125
|
+
email: params[:email],
|
126
|
+
password: params[:password],
|
127
|
+
password_confirmation: params[:password_confirmation],
|
128
|
+
}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ApiEngineBase
|
2
|
+
class UsernameController < ::ApiEngineBase::ApplicationController
|
3
|
+
|
4
|
+
# GET /username/available/:username
|
5
|
+
def username_availability
|
6
|
+
result = ApiEngineBase::Username::Available.(username: params[:username])
|
7
|
+
|
8
|
+
if result.success?
|
9
|
+
json_result = {
|
10
|
+
username: {
|
11
|
+
available: result.available,
|
12
|
+
valid: result.valid,
|
13
|
+
description: ApiEngineBase.config.username.username_failure_message
|
14
|
+
}
|
15
|
+
}
|
16
|
+
status = 200
|
17
|
+
else
|
18
|
+
json_result = { msg: result.msg }
|
19
|
+
json_result[:invalid_arguments] = true if result.invalid_arguments
|
20
|
+
status = 401
|
21
|
+
end
|
22
|
+
|
23
|
+
render json: json_result, status: status
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase
|
4
|
+
module SchemaHelper
|
5
|
+
def schema_succesful!(schema:, status:)
|
6
|
+
render(json: schema.to_h, status:)
|
7
|
+
end
|
8
|
+
|
9
|
+
def invalid_arguments!(message:, argument_object:, schema:, status:)
|
10
|
+
bad_arguments = argument_object.map do |key, metadata|
|
11
|
+
ApiEngineBase::Schema::Error::InvalidArgument.new(
|
12
|
+
schema:,
|
13
|
+
argument: key,
|
14
|
+
argument_type: metadata[:type],
|
15
|
+
reason: metadata[:msg],
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
result = ApiEngineBase::Schema::Error::InvalidArgumentResponse.new(
|
20
|
+
invalid_arguments: bad_arguments,
|
21
|
+
invalid_argument_keys: argument_object.keys,
|
22
|
+
status:,
|
23
|
+
message:,
|
24
|
+
)
|
25
|
+
|
26
|
+
render(json: result.to_h, status:)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/app/models/user.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: users
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# email :string(255) default(""), not null
|
9
|
+
# email_validated :boolean default(FALSE)
|
10
|
+
# first_name :string(255) default(""), not null
|
11
|
+
# last_known_timezone :string(255)
|
12
|
+
# last_known_timezone_update :datetime
|
13
|
+
# last_login :datetime
|
14
|
+
# last_login_strategy :string(255)
|
15
|
+
# last_name :string(255) default(""), not null
|
16
|
+
# password_consecutive_fail :integer default(0)
|
17
|
+
# password_digest :string(255) default(""), not null
|
18
|
+
# recovery_password_digest :string(255) default(""), not null
|
19
|
+
# successful_login :integer default(0)
|
20
|
+
# username :string(255)
|
21
|
+
# verifier_token :string(255)
|
22
|
+
# verifier_token_last_reset :datetime
|
23
|
+
# created_at :datetime not null
|
24
|
+
# updated_at :datetime not null
|
25
|
+
#
|
26
|
+
# Indexes
|
27
|
+
#
|
28
|
+
# index_users_on_username (username) UNIQUE
|
29
|
+
#
|
30
|
+
require "securerandom"
|
31
|
+
|
32
|
+
class User < ApiEngineBase::ApplicationRecord
|
33
|
+
has_secure_password
|
34
|
+
|
35
|
+
validates :username, uniqueness: true
|
36
|
+
validates :email, uniqueness: true
|
37
|
+
|
38
|
+
def full_name
|
39
|
+
"#{first_name} #{last_name}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def retreive_verifier_token!
|
43
|
+
return verifier_token if verifier_token
|
44
|
+
|
45
|
+
value = SecureRandom.alphanumeric(32)
|
46
|
+
update!(verifier_token: value)
|
47
|
+
|
48
|
+
value
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: user_secrets
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# death_time :datetime
|
9
|
+
# extra :string(255)
|
10
|
+
# reason :string(255)
|
11
|
+
# secret :string(255)
|
12
|
+
# use_count :integer default(0)
|
13
|
+
# use_count_max :integer
|
14
|
+
# created_at :datetime not null
|
15
|
+
# updated_at :datetime not null
|
16
|
+
# user_id :bigint not null
|
17
|
+
#
|
18
|
+
# Indexes
|
19
|
+
#
|
20
|
+
# index_user_secrets_on_secret (secret) UNIQUE
|
21
|
+
# index_user_secrets_on_user_id (user_id)
|
22
|
+
#
|
23
|
+
# Foreign Keys
|
24
|
+
#
|
25
|
+
# fk_rails_... (user_id => users.id)
|
26
|
+
#
|
27
|
+
class UserSecret < ApplicationRecord
|
28
|
+
belongs_to :user
|
29
|
+
|
30
|
+
def self.find_record(secret:, reason: nil, access_count: true)
|
31
|
+
params = { secret:, reason: }.compact
|
32
|
+
record = where(**params).first
|
33
|
+
return { found: false } if record.nil?
|
34
|
+
|
35
|
+
record.access_count! if access_count
|
36
|
+
|
37
|
+
{
|
38
|
+
found: true,
|
39
|
+
valid: record.is_valid?,
|
40
|
+
record: record,
|
41
|
+
user: record.user,
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def invalid_reason
|
46
|
+
arr = []
|
47
|
+
arr << "Expired secret." if !still_alive?
|
48
|
+
arr << "Secret used too many times." if !valid_use_count?
|
49
|
+
|
50
|
+
arr
|
51
|
+
end
|
52
|
+
|
53
|
+
def access_count!
|
54
|
+
update(use_count: use_count + 1)
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_valid?
|
58
|
+
valid_use_count? && still_alive?
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid_use_count?
|
62
|
+
return true if use_count_max.nil?
|
63
|
+
|
64
|
+
use_count <= use_count_max
|
65
|
+
end
|
66
|
+
|
67
|
+
def still_alive?
|
68
|
+
return true if death_time.nil?
|
69
|
+
|
70
|
+
death_time > Time.now
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiEngineBase::ArgumentValidation
|
4
|
+
module ClassMethods
|
5
|
+
ON_ARGUMENT_VALIDATION = [
|
6
|
+
DEFAULT_VALIDATION = :raise,
|
7
|
+
:fail_early,
|
8
|
+
:log,
|
9
|
+
]
|
10
|
+
|
11
|
+
def on_argument_validation_assigned
|
12
|
+
@on_argument_validation ||= DEFAULT_VALIDATION
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_argument_validation(set)
|
16
|
+
raise "Must be one of #{ON_ARGUMENT_VALIDATION}" unless ON_ARGUMENT_VALIDATION.include?(set)
|
17
|
+
|
18
|
+
@on_argument_validation = set
|
19
|
+
end
|
20
|
+
|
21
|
+
def compose_exact(count, name, required:, delegation: false, &block)
|
22
|
+
raise ApiEngineBase::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
|
23
|
+
|
24
|
+
validation_proc = Proc.new do |input_count, keys|
|
25
|
+
|
26
|
+
language = (input_count > 1) ? "But more than #{count} did" : "But no key had a value"
|
27
|
+
{
|
28
|
+
message: "Expected [#{count}] of the keys to have a value assigned. #{language}",
|
29
|
+
is_valid: (input_count == count),
|
30
|
+
requirement: "Exactly #{count} key(s) of #{keys} must be provided.",
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
composition_validation_proc = Proc.new do
|
35
|
+
keys = compositions[__stacked_type.last[:name]][:keys]
|
36
|
+
if keys.length < count
|
37
|
+
raise ApiEngineBase::ServiceBase::CompositionValidationError, "Composition configuration error. Key [#{name}] expects EXACTLY #{count} keys to create an instance. Only #{keys.length} is provided for the composition. Please add more keys or reduce the expectation"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
composition(name:, type: :compose_exact, required:, delegation:, composition_validation_proc:, validation_proc:, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def at_most(count, name, required:, &block)
|
44
|
+
raise ApiEngineBase::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
|
45
|
+
|
46
|
+
validation_proc = Proc.new do |input_count, keys|
|
47
|
+
{
|
48
|
+
message: "Expected at most #{count} keys assigned",
|
49
|
+
is_valid: (input_count <= count),
|
50
|
+
requirement: "Validation Error: At most #{count} key(s) of #{keys} can be provided.",
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
composition(name:, type: :at_most, required:, delegation: false, validation_proc:, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
def at_least(count, name, required:, &block)
|
58
|
+
raise ApiEngineBase::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
|
59
|
+
|
60
|
+
validation_proc = Proc.new do |input_count, keys|
|
61
|
+
{
|
62
|
+
message: "Expected at least #{count} keys assigned. Available keys",
|
63
|
+
is_valid: (input_count >= count),
|
64
|
+
requirement: "Validation Error: At least #{count} key(s) of #{keys} must be provided.",
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
composition_validation_proc = Proc.new do
|
69
|
+
keys = compositions[__stacked_type.last[:name]][:keys]
|
70
|
+
if keys.length < count
|
71
|
+
raise ApiEngineBase::ServiceBase::CompositionValidationError, "Composition configuration error. Key [#{name}] expects AT LEAST #{count} keys to create an instance. Only #{keys.length} is provided for the composition. Please add more keys or reduce the expectation"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
composition(name:, type: :at_least, required:, delegation: false, composition_validation_proc:, validation_proc:, &block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def at_least_one(name, required:, &block)
|
78
|
+
at_least(1, name, required:, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
def one_of(name, required:, delegation: true, &block)
|
82
|
+
compose_exact(1, name, required:, delegation:, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def composition(name:, type:, required:, delegation:, validation_proc:, composition_validation_proc: nil, &block)
|
86
|
+
compositions[name] ||= { type:, name:, keys: [], required:, delegation:, validation_proc: }
|
87
|
+
if __stacked_type.map { _1[:type] }.include?(type)
|
88
|
+
raise ApiEngineBase::ServiceBase::NestedDuplicateTypeError, "Duplicate Nested type's are not allowed. #{type} composition was included more than once"
|
89
|
+
end
|
90
|
+
|
91
|
+
__stacked_type << { type:, name: }
|
92
|
+
|
93
|
+
yield
|
94
|
+
|
95
|
+
if composition_validation_proc
|
96
|
+
composition_validation_proc.()
|
97
|
+
end
|
98
|
+
|
99
|
+
if __existing_names.include?(name)
|
100
|
+
raise ApiEngineBase::ServiceBase::NameConflictError, "Name conflict for #{name}. Duplicated as a key"
|
101
|
+
end
|
102
|
+
|
103
|
+
__existing_names << name
|
104
|
+
# returning from the yield...pop it from the stack.
|
105
|
+
# This allows us to know the current depth of where we are in the nested stack
|
106
|
+
__stacked_type.pop
|
107
|
+
|
108
|
+
if delegation
|
109
|
+
delegate name, to: :context
|
110
|
+
delegate :"#{name}_key", to: :context
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate(name, default: nil, length: false, matches: nil, is_a: nil, is_one: nil, lt: nil, lte: nil, eq: nil, gt: nil, gte: nil, delegation: true, sensitive: false, required: false)
|
115
|
+
if __existing_names.include?(name)
|
116
|
+
raise ApiEngineBase::ServiceBase::NameConflictError, "Duplicate key name found. [#{name}] can only be defined once"
|
117
|
+
end
|
118
|
+
|
119
|
+
__existing_names << name
|
120
|
+
|
121
|
+
if default
|
122
|
+
if is_a
|
123
|
+
if Array(is_a).none? { _1 === default }
|
124
|
+
raise ApiEngineBase::ServiceBase::DefaultValueError, "Default value provided [#{default}] does not match any `is_a` value(s) of #{is_a}."
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
if is_one
|
129
|
+
if Array(is_one).none? { _1 == default }
|
130
|
+
raise ApiEngineBase::ServiceBase::DefaultValueError, "Default value provided [#{default}] does not match any `is_one` value(s) of #{is_one}."
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
if __stacked_type.length > 0
|
136
|
+
compositions[__stacked_type.last[:name]][:keys] << name
|
137
|
+
end
|
138
|
+
|
139
|
+
validate_params << {
|
140
|
+
name:,
|
141
|
+
is_a:,
|
142
|
+
lt:,
|
143
|
+
lte:,
|
144
|
+
eq:,
|
145
|
+
gt:,
|
146
|
+
gte:,
|
147
|
+
length:,
|
148
|
+
required:,
|
149
|
+
is_one:,
|
150
|
+
default:,
|
151
|
+
}
|
152
|
+
sensitive_params << name if sensitive
|
153
|
+
|
154
|
+
if delegation
|
155
|
+
delegate name, to: :context
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def sensitive_params
|
160
|
+
@sensitive_params ||= []
|
161
|
+
end
|
162
|
+
|
163
|
+
def validate_params
|
164
|
+
@validate_params ||= []
|
165
|
+
end
|
166
|
+
|
167
|
+
def compositions
|
168
|
+
@compositions ||= {}
|
169
|
+
end
|
170
|
+
|
171
|
+
def __stacked_type
|
172
|
+
@__stacked_type ||= []
|
173
|
+
end
|
174
|
+
|
175
|
+
def __existing_names
|
176
|
+
@__existing_names ||= []
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|