api_engine_base 0.1.1
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.
- 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
|