ru.Bee 1.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/bin/rubee +259 -0
- data/lib/Dockerfile +33 -0
- data/lib/app/controllers/welcome_controller.rb +5 -0
- data/lib/app/models/user.rb +6 -0
- data/lib/app/views/welcome_show.erb +52 -0
- data/lib/config/base_configuration.rb +11 -0
- data/lib/config/routes.rb +3 -0
- data/lib/config.ru +3 -0
- data/lib/db/create_users.rb +16 -0
- data/lib/db/structure.rb +34 -0
- data/lib/db/test.db +0 -0
- data/lib/images/rubee.svg +6 -0
- data/lib/inits/print_colors.rb +24 -0
- data/lib/rubee/async/asyncable.rb +17 -0
- data/lib/rubee/async/sidekiq_async.rb +15 -0
- data/lib/rubee/async/thread_async.rb +8 -0
- data/lib/rubee/async/thread_pool.rb +53 -0
- data/lib/rubee/controllers/base_controller.rb +78 -0
- data/lib/rubee/controllers/extensions/auth_tokenable.rb +81 -0
- data/lib/rubee/controllers/extensions/middlewarable.rb +31 -0
- data/lib/rubee/controllers/middlewares/auth_token_middleware.rb +42 -0
- data/lib/rubee/extensions/hookable.rb +85 -0
- data/lib/rubee/extensions/serializable.rb +28 -0
- data/lib/rubee/models/database_object.rb +50 -0
- data/lib/rubee/models/sequel_object.rb +86 -0
- data/lib/rubee/tests/auth_tokenable_test.rb +29 -0
- data/lib/rubee/tests/rubeeapp_test.rb +24 -0
- data/lib/rubee/tests/test_helper.rb +11 -0
- data/lib/rubee/tests/user_model_test.rb +51 -0
- data/lib/rubee.rb +259 -0
- data/lib/version.rb +1 -0
- data/readme.md +353 -0
- metadata +95 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative File.join(__dir__, 'middlewarable')
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Rubee
|
5
|
+
module AuthTokenable
|
6
|
+
KEY = "secret#{Date.today}".freeze # Feel free to cusomtize it
|
7
|
+
EXPIRE = 3600 # 1 hour
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.include(Middlewarable)
|
11
|
+
base.include(InstanceMethods)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
|
14
|
+
base.attach('AuthTokenMiddleware')
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
def authentificated?
|
19
|
+
methods = self.class._auth_methods
|
20
|
+
return true if methods && !methods.include?(@route[:action].to_sym)
|
21
|
+
# This is suppose to be set in the middleware, otherwise it will return false
|
22
|
+
valid_token?
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid_token?
|
26
|
+
@request.env["rack.session"]&.[]("authentificated")
|
27
|
+
end
|
28
|
+
|
29
|
+
def authentificated_user
|
30
|
+
# User model must be created with email and password properties at least
|
31
|
+
@authehtificated_user ||= User.where(email: params[:email], password: params[:password]).first
|
32
|
+
end
|
33
|
+
|
34
|
+
def authentificate!
|
35
|
+
return false unless authentificated_user
|
36
|
+
# Generate token
|
37
|
+
payload = { username: params[:email], exp: Time.now.to_i + EXPIRE }
|
38
|
+
@token = JWT.encode(payload, KEY, 'HS256')
|
39
|
+
# Set jwt token to the browser within cookie, so next browser request will include it.
|
40
|
+
# make sure it passed to response_with headers options
|
41
|
+
@token_header = { "set-cookie" => "jwt=#{@token}; path=/; httponly; secure" }
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def unauthentificate!
|
47
|
+
@request.env["rack.session"]["authentificated"] = nil if @request.env["rack.session"]&.[]("authentificated")
|
48
|
+
@authehtificated_user = nil if @authehtificated_user
|
49
|
+
@zeroed_token_header = {
|
50
|
+
"set-cookie" => "jwt=; path=/; httponly; secure; expires=thu, 01 jan 1970 00:00:00 gmt",
|
51
|
+
"content-type" => "application/json"
|
52
|
+
}
|
53
|
+
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_auth
|
58
|
+
if authentificated?
|
59
|
+
yield
|
60
|
+
else
|
61
|
+
response_with type: :unauthentificated
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module ClassMethods
|
67
|
+
def auth_methods(*args)
|
68
|
+
@auth_methods ||= []
|
69
|
+
@auth_methods.concat(args.map(&:to_sym)).uniq!
|
70
|
+
|
71
|
+
@auth_methods.each do |method|
|
72
|
+
around method, :handle_auth
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def _auth_methods
|
77
|
+
@auth_methods || []
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Rubee system file
|
2
|
+
# WARNING: DO NOT EDIT THIS FILE UNLESS YOU FEEL STRONG DESIRE
|
3
|
+
# Unpreditable behaviour may happen. Take it as your own risk.
|
4
|
+
module Middlewarable
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
base.prepend(Initializer)
|
8
|
+
end
|
9
|
+
|
10
|
+
module Initializer
|
11
|
+
def initialize(req, route)
|
12
|
+
app = ->(env) { super(req, route) }
|
13
|
+
self.class.middlewares.reverse_each do |middleware|
|
14
|
+
middleware_class = Object.const_get(middleware)
|
15
|
+
app = middleware_class.new(app, req)
|
16
|
+
end
|
17
|
+
app.call(req.env)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def attach(*args)
|
23
|
+
@middlewares ||= []
|
24
|
+
@middlewares.concat(args).uniq
|
25
|
+
end
|
26
|
+
|
27
|
+
def middlewares
|
28
|
+
@middlewares || []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Rubee
|
2
|
+
class AuthTokenMiddleware
|
3
|
+
def initialize(app, req)
|
4
|
+
@req = req
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
# get token from header
|
10
|
+
auth_header = headers(env)["HTTP_AUTHORIZATION"]
|
11
|
+
token = auth_header ? auth_header[/^Bearer (.*)$/]&.gsub("Bearer ", "") : nil
|
12
|
+
# get token from cookies
|
13
|
+
token = @req.cookies["jwt"] unless token
|
14
|
+
if valid_token?(token)
|
15
|
+
env["rack.session"] ||= {}
|
16
|
+
env["rack.session"]["authentificated"] = true
|
17
|
+
end
|
18
|
+
|
19
|
+
@app.call(env)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def headers(env)
|
25
|
+
env.each_with_object({}) { |(k, v), h| h[k] = v if k.start_with?("HTTP_") }
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid_token?(token)
|
29
|
+
return false unless token
|
30
|
+
|
31
|
+
hash = decode_jwt(token)
|
32
|
+
email = hash[:username]
|
33
|
+
|
34
|
+
User.where(email:)&.any? if email
|
35
|
+
end
|
36
|
+
|
37
|
+
def decode_jwt(token)
|
38
|
+
decoded_array = JWT.decode(token, AuthTokenable::KEY, true, { algorithm: 'HS256' }) rescue decoded_array = []
|
39
|
+
decoded_array&.first&.transform_keys(&:to_sym) || {} # Extract payload
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Rubee
|
2
|
+
module Hookable
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
base.include(InstanceMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def before(method, handler, **options)
|
10
|
+
hooks = Module.new do
|
11
|
+
define_method(method) do |*args, &block|
|
12
|
+
if conditions_met?(options[:if], options[:unless])
|
13
|
+
handler.respond_to?(:call) ? handler.call : send(handler)
|
14
|
+
end
|
15
|
+
|
16
|
+
super(*args, &block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
prepend hooks
|
20
|
+
end
|
21
|
+
|
22
|
+
def after(method, handler, **options)
|
23
|
+
hooks = Module.new do
|
24
|
+
define_method(method) do |*args, &block|
|
25
|
+
result = super(*args, &block)
|
26
|
+
|
27
|
+
if conditions_met?(options[:if], options[:unless])
|
28
|
+
handler.respond_to?(:call) ? handler.call : send(handler)
|
29
|
+
end
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
prepend hooks
|
35
|
+
end
|
36
|
+
|
37
|
+
def around(method, handler, **options)
|
38
|
+
hooks = Module.new do
|
39
|
+
define_method(method) do |*args, &block|
|
40
|
+
if conditions_met?(options[:if], options[:unless])
|
41
|
+
if handler.respond_to?(:call)
|
42
|
+
handler.call { super(*args, &block) }
|
43
|
+
else
|
44
|
+
send(handler) { super(*args, &block) }
|
45
|
+
end
|
46
|
+
else
|
47
|
+
super(*args, &block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
prepend hooks
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module InstanceMethods
|
56
|
+
private
|
57
|
+
|
58
|
+
def conditions_met?(if_condition = nil, unless_condition = nil)
|
59
|
+
return true if if_condition.nil? && unless_condition.nil?
|
60
|
+
|
61
|
+
if_condition_result = true
|
62
|
+
if_condition_result =
|
63
|
+
if if_condition.nil?
|
64
|
+
true
|
65
|
+
elsif if_condition.respond_to?(:call)
|
66
|
+
if_condition.call
|
67
|
+
elsif respond_to?(if_condition)
|
68
|
+
send(if_condition)
|
69
|
+
end
|
70
|
+
|
71
|
+
unless_condition_result = true
|
72
|
+
unless_condition_result =
|
73
|
+
if unless_condition.nil?
|
74
|
+
false
|
75
|
+
elsif unless_condition.respond_to?(:call)
|
76
|
+
unless_condition.call
|
77
|
+
elsif respond_to?(unless_condition)
|
78
|
+
send(unless_condition)
|
79
|
+
end
|
80
|
+
|
81
|
+
if_condition_result && !unless_condition_result
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Rubee
|
2
|
+
module Serializable
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:include, InstanceMethods)
|
5
|
+
base.prepend(Initializer)
|
6
|
+
end
|
7
|
+
|
8
|
+
module Initializer
|
9
|
+
def initialize(attrs)
|
10
|
+
attrs.each do |attr, value|
|
11
|
+
self.send("#{attr}=", value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
def to_json
|
18
|
+
to_h.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
instance_variables.each_with_object({}) do |var, hash|
|
23
|
+
hash[var.to_s.delete("@")] = instance_variable_get(var)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Rubee
|
2
|
+
class DatabaseObject
|
3
|
+
include Serializable
|
4
|
+
include Hookable
|
5
|
+
|
6
|
+
def destroy
|
7
|
+
end
|
8
|
+
|
9
|
+
def save
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(args = {})
|
13
|
+
end
|
14
|
+
|
15
|
+
def reload
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def last
|
20
|
+
end
|
21
|
+
|
22
|
+
def connection
|
23
|
+
end
|
24
|
+
|
25
|
+
def all
|
26
|
+
end
|
27
|
+
|
28
|
+
def find(id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def where(args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create(attrs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def pluralize_class_name
|
38
|
+
word = self.name.downcase
|
39
|
+
# Basic pluralization rules
|
40
|
+
if word.end_with?('y') && !%w[a e i o u].include?(word[-2])
|
41
|
+
word[0..-2] + 'ies' # Replace "y" with "ies"
|
42
|
+
elsif word.end_with?('s', 'x', 'z', 'ch', 'sh')
|
43
|
+
word + 'es' # Add "es" for certain endings
|
44
|
+
else
|
45
|
+
word + 's' # Default to adding "s"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Rubee
|
2
|
+
class SequelObject < DatabaseObject
|
3
|
+
DB = Sequel.connect(Rubee::Configuration.get_database_url) rescue nil
|
4
|
+
|
5
|
+
def destroy
|
6
|
+
self.class.connection.where(id:).delete
|
7
|
+
end
|
8
|
+
|
9
|
+
def save
|
10
|
+
args = to_h.dup&.transform_keys(&:to_sym)
|
11
|
+
if args[:id]
|
12
|
+
udpate(args) rescue return false
|
13
|
+
|
14
|
+
true
|
15
|
+
else
|
16
|
+
created_object = self.class.create(args) rescue return false
|
17
|
+
self.id = created_object.id
|
18
|
+
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def assign_attributes(args={})
|
24
|
+
to_h.each do |attr, value|
|
25
|
+
self.send("#{attr}=", args[attr.to_sym]) if args[attr.to_sym]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def update(args = {})
|
30
|
+
assign_attributes(args)
|
31
|
+
found_hash = self.class.connection.where(id:)
|
32
|
+
return self.class.find(id) if found_hash&.update(**args)
|
33
|
+
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def persisted?
|
38
|
+
!!id
|
39
|
+
end
|
40
|
+
|
41
|
+
def reload
|
42
|
+
self.class.find(id)
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
def last
|
47
|
+
found_hash = connection.order(:id).last
|
48
|
+
return self.new(**found_hash) if found_hash
|
49
|
+
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def connection
|
54
|
+
@connection ||= DB[pluralize_class_name.to_sym]
|
55
|
+
end
|
56
|
+
|
57
|
+
def all
|
58
|
+
connection.map do |record_hash|
|
59
|
+
self.new(**record_hash)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def find(id)
|
64
|
+
found_hash = connection.where(id:)&.first
|
65
|
+
return self.new(**found_hash) if found_hash
|
66
|
+
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def where(args)
|
71
|
+
connection.where(**args).map do |record_hash|
|
72
|
+
self.new(**record_hash)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def create(attrs)
|
77
|
+
out_id = connection.insert(**attrs)
|
78
|
+
self.new(**(attrs.merge(id: out_id)))
|
79
|
+
end
|
80
|
+
|
81
|
+
def destroy_all
|
82
|
+
all.each(&:destroy)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class RubeeAppTest < Minitest::Test
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def app
|
7
|
+
Rubee::Application.instance
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup
|
11
|
+
Rubee::Autoload.call
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
# detach auth methods
|
16
|
+
if WelcomeController.instance_variable_defined?(:@auth_methods)
|
17
|
+
WelcomeController.send(:remove_instance_variable, :@auth_methods)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_welcome_controller_included_auth_tokenable
|
22
|
+
WelcomeController.include(AuthTokenable)
|
23
|
+
WelcomeController.auth_methods :show
|
24
|
+
|
25
|
+
get '/'
|
26
|
+
|
27
|
+
assert_equal last_response.status, 401
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class RubeeAppTest < Minitest::Test
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def app
|
7
|
+
Rubee::Application.instance
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def test_welcome_route
|
12
|
+
get '/'
|
13
|
+
|
14
|
+
assert_equal 200, last_response.status, "Unexpected response: #{last_response.body}"
|
15
|
+
assert_includes last_response.body, 'All set up and running!'
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def test_not_found_route
|
20
|
+
get '/random'
|
21
|
+
|
22
|
+
assert_equal 404, last_response.status
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe 'User model' do
|
4
|
+
describe ".create" do
|
5
|
+
after do
|
6
|
+
User.destroy_all
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'when data is valid' do
|
10
|
+
it 'persists to db' do
|
11
|
+
user = User.create(email: "ok-test@test.com", password: "123")
|
12
|
+
|
13
|
+
_(user.persisted?).must_equal true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'when data is invalid' do
|
18
|
+
it 'is not changing users number' do
|
19
|
+
initial_count = User.all.count
|
20
|
+
User.create(wrong: "test@test") rescue nil
|
21
|
+
|
22
|
+
_(User.all.count).must_equal initial_count
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '.save' do
|
28
|
+
after do
|
29
|
+
User.destroy_all
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'when data is valid' do
|
33
|
+
it 'persists to db' do
|
34
|
+
user = User.new(email: "ok-test@test.com", password: "123")
|
35
|
+
user.save
|
36
|
+
|
37
|
+
_(user.persisted?).must_equal true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'when data is invalid' do
|
42
|
+
it 'is not changing users number' do
|
43
|
+
initial_count = User.all.count
|
44
|
+
user = User.new(wrong: "test@test") rescue nil
|
45
|
+
user.save rescue nil
|
46
|
+
|
47
|
+
_(User.all.count).must_equal initial_count
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|