avo 0.2.2 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of avo might be problematic. Click here for more details.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +49 -48
  3. data/Gemfile.lock +19 -2
  4. data/README.md +30 -9
  5. data/app/controllers/avo/application_controller.rb +34 -1
  6. data/app/controllers/avo/filters_controller.rb +19 -0
  7. data/app/controllers/avo/relations_controller.rb +34 -0
  8. data/app/controllers/avo/resource_overview_controller.rb +14 -7
  9. data/app/controllers/avo/resources_controller.rb +66 -142
  10. data/app/controllers/avo/search_controller.rb +55 -0
  11. data/app/helpers/avo/application_helper.rb +6 -2
  12. data/app/views/layouts/avo/_javascript.html.erb +1 -0
  13. data/app/views/layouts/avo/application.html.erb +34 -17
  14. data/app/views/partials/_header.html.erb +1 -1
  15. data/avo.gemspec +4 -2
  16. data/config/credentials.yml.enc +1 -0
  17. data/config/routes.rb +11 -7
  18. data/lib/avo.rb +2 -0
  19. data/lib/avo/app/app.rb +18 -29
  20. data/lib/avo/app/authorization_service.rb +40 -0
  21. data/lib/avo/app/fields/has_and_belongs_to_many.rb +1 -0
  22. data/lib/avo/app/fields/has_many.rb +1 -0
  23. data/lib/avo/app/fields/id_field.rb +4 -4
  24. data/lib/avo/app/licensing/community_license.rb +4 -0
  25. data/lib/avo/app/licensing/hq.rb +85 -0
  26. data/lib/avo/app/licensing/license.rb +48 -0
  27. data/lib/avo/app/licensing/license_manager.rb +25 -0
  28. data/lib/avo/app/licensing/null_license.rb +12 -0
  29. data/lib/avo/app/licensing/pro_license.rb +9 -0
  30. data/lib/avo/app/resource.rb +32 -12
  31. data/lib/avo/configuration.rb +14 -0
  32. data/lib/avo/engine.rb +3 -3
  33. data/lib/avo/version.rb +1 -1
  34. data/lib/generators/avo/templates/initializer.rb +2 -0
  35. data/lib/generators/avo/templates/views/_header.html.erb +1 -1
  36. data/lib/generators/avo/templates/views/_scripts.html.erb +0 -0
  37. data/public/avo-packs/css/application-73e568bc.css +3 -0
  38. data/public/avo-packs/css/application-73e568bc.css.br +0 -0
  39. data/public/avo-packs/css/application-73e568bc.css.gz +0 -0
  40. data/public/avo-packs/js/application-725ee54e3adbcd950843.js +3 -0
  41. data/public/avo-packs/js/{application-9a0dde96ad9918852965.js.LICENSE.txt → application-725ee54e3adbcd950843.js.LICENSE.txt} +0 -0
  42. data/public/avo-packs/js/application-725ee54e3adbcd950843.js.br +0 -0
  43. data/public/avo-packs/js/application-725ee54e3adbcd950843.js.gz +0 -0
  44. data/public/avo-packs/js/application-725ee54e3adbcd950843.js.map +1 -0
  45. data/public/avo-packs/js/application-725ee54e3adbcd950843.js.map.br +0 -0
  46. data/public/avo-packs/js/application-725ee54e3adbcd950843.js.map.gz +0 -0
  47. data/public/avo-packs/manifest.json +8 -6
  48. data/public/avo-packs/manifest.json.br +0 -0
  49. data/public/avo-packs/manifest.json.gz +0 -0
  50. data/public/avo-packs/media/svgs/arrow-circle-right-1ad1e15ec9a7aa54b67d126566a5aa2d.svg +1 -0
  51. data/public/avo-packs/media/svgs/arrow-circle-right-1ad1e15ec9a7aa54b67d126566a5aa2d.svg.br +0 -0
  52. data/public/avo-packs/media/svgs/arrow-circle-right-1ad1e15ec9a7aa54b67d126566a5aa2d.svg.gz +0 -0
  53. data/public/avo-packs/media/svgs/exclamation-8d1c0baa390a8df9bb52176011eb5892.svg +1 -0
  54. data/public/avo-packs/media/svgs/exclamation-8d1c0baa390a8df9bb52176011eb5892.svg.br +0 -0
  55. data/public/avo-packs/media/svgs/exclamation-8d1c0baa390a8df9bb52176011eb5892.svg.gz +0 -0
  56. metadata +61 -21
  57. data/public/avo-packs/css/application-5dc4dd78.css +0 -3
  58. data/public/avo-packs/css/application-5dc4dd78.css.br +0 -0
  59. data/public/avo-packs/css/application-5dc4dd78.css.gz +0 -0
  60. data/public/avo-packs/js/application-9a0dde96ad9918852965.js +0 -3
  61. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.br +0 -0
  62. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.gz +0 -0
  63. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.map +0 -1
  64. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.map.br +0 -0
  65. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.map.gz +0 -0
@@ -0,0 +1,55 @@
1
+ require_dependency 'avo/application_controller'
2
+
3
+ module Avo
4
+ class SearchController < ApplicationController
5
+ before_action :authorize_user
6
+
7
+ def index
8
+ resources = []
9
+
10
+ resources_to_search_through = App.get_resources
11
+ .select { |resource| resource.search.present? }
12
+ .select { |resource| AuthorizationService.authorize_action current_user, resource.model, 'index' }
13
+ .each do |resource_model|
14
+ found_resources = add_link_to_search_results(search_resource(resource_model), resource_model)
15
+ resources.push({
16
+ label: resource_model.name,
17
+ resources: found_resources
18
+ })
19
+ end
20
+
21
+ render json: {
22
+ resources: resources
23
+ }
24
+ end
25
+
26
+ def resource
27
+ render json: {
28
+ resources: add_link_to_search_results(search_resource(avo_resource), avo_resource)
29
+ }
30
+ end
31
+
32
+ private
33
+ def add_link_to_search_results(resources, avo_resource)
34
+ resources.map do |model|
35
+ {
36
+ id: model.id,
37
+ search_label: model.send(avo_resource.title),
38
+ link: "/resources/#{model.class.to_s.singularize.underscore}/#{model.id}",
39
+ }
40
+ end
41
+ end
42
+
43
+ def search_resource(avo_resource)
44
+ avo_resource.query_search(query: params[:q], via_resource_name: params[:via_resource_name], via_resource_id: params[:via_resource_id], user: current_user)
45
+ end
46
+
47
+ def authorize_user
48
+ return if params[:action] == 'index'
49
+
50
+ action = params[:action] == 'resource' ? :index : params[:action]
51
+
52
+ return render_unauthorized unless AuthorizationService::authorize_action current_user, avo_resource.model, action
53
+ end
54
+ end
55
+ end
@@ -10,12 +10,16 @@ module Avo
10
10
  render partial: 'vendor/avo/partials/logo' rescue render partial: 'partials/logo'
11
11
  end
12
12
 
13
+ def render_header
14
+ render partial: 'vendor/avo/partials/header' rescue render partial: 'partials/header'
15
+ end
16
+
13
17
  def render_footer
14
18
  render partial: 'vendor/avo/partials/footer' rescue render partial: 'partials/footer'
15
19
  end
16
20
 
17
- def render_header
18
- render partial: 'vendor/avo/partials/header' rescue render partial: 'partials/header'
21
+ def render_scripts
22
+ render partial: 'vendor/avo/partials/scripts' rescue ''
19
23
  end
20
24
  end
21
25
  end
@@ -2,4 +2,5 @@
2
2
  var rootPath = '<%= Avo.configuration.root_path %>';
3
3
  var timezone = '<%= Avo.configuration.timezone %>';
4
4
  var defaultViewType = '<%= Avo.configuration.default_view_type %>';
5
+ var license = <%= Avo::App.license.properties.to_json.html_safe %>;
5
6
  </script>
@@ -13,27 +13,44 @@
13
13
  <body class="bg-gray-200">
14
14
 
15
15
  <div id="app" class="flex min-h-screen flex-row h-full">
16
- <application-sidebar :resources='<%= Avo::App.get_resources_navigation.as_json.html_safe %>'>
17
- <%= render_logo %>
18
- </application-sidebar>
16
+ <app-layout class="flex flex-1 w-full"
17
+ :router-key="routerKey"
18
+ :layout="layout"
19
+ inline-template
20
+ >
21
+ <div>
22
+ <application-sidebar :resources='<%= Avo::App.get_resources_navigation(current_user).as_json.html_safe %>' v-if="layout !== 'blank'">
23
+ <template #logo>
24
+ <%= render_logo %>
25
+ </template>
26
+ <template #licensing>
27
+ <license-warnings></license-warnings>
28
+ </template>
29
+ </application-sidebar>
19
30
 
20
- <div class="flex-1 h-full overflow-auto">
21
- <div class="relative bg-white p-2 shadow-md h-16 w-full flex items-center z-40">
22
- <div class="ml-6">
23
- <%= render_header %>
24
- </div>
25
- <div class="flex-1 flex justify-center">
26
- <div class="w-64">
27
- <resources-search :global="true">
31
+ <div class="flex-1 h-full overflow-auto">
32
+ <div class="relative bg-white p-2 shadow-md h-16 w-full flex items-center z-40" v-if="layout !== 'blank'">
33
+ <div class="ml-6">
34
+ <%= render_header %>
35
+ </div>
36
+ <div class="flex-1 flex justify-center">
37
+ <div class="w-64">
38
+ <resources-search :global="true">
39
+ </div>
40
+ </div>
28
41
  </div>
29
- </div>
30
- </div>
31
42
 
32
- <div class="content p-8">
33
- <%= yield %>
34
- <%= render_footer %>
43
+ <div v-if="layout === 'blank'" class="h-full w-full flex justify-center items-center">
44
+ <%= yield %>
45
+ </div>
46
+ <div class="content p-8" v-else>
47
+ <%= yield %>
48
+ <%= render_footer %>
49
+ </div>
50
+ </div>
35
51
  </div>
36
- </div>
52
+ </app-layout>
37
53
  </div>
54
+ <%= render_scripts %>
38
55
  </body>
39
56
  </html>
@@ -1 +1 @@
1
- <%= link_to Avo.configuration.app_name, main_app.root_path, class: 'text-green-600 font-semibold', target: :_blank %>
1
+ <%= link_to Avo.configuration.app_name, '/', class: 'text-green-600 font-semibold', target: :_blank %>
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.name = 'avo'
9
9
  spec.version = Avo::VERSION
10
10
  spec.authors = ['Adrian Marin', 'Mihai Marin']
11
- spec.email = ['adrian@avohq.io']
11
+ spec.email = ['avo@avohq.io']
12
12
  spec.homepage = 'https://avohq.io'
13
13
  spec.summary = 'Configuration-based, no-maintenance, extendable Ruby on Rails admin.'
14
14
  spec.description = 'Avo is a beautiful next-generation framework that empowers you, the developer, to create fantastic admin panels for your Ruby on Rails apps with the flexibility to fit your needs as you grow.'
@@ -32,10 +32,12 @@ Gem::Specification.new do |spec|
32
32
  spec.files = Dir['{bin,app,config,db,lib,public}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md', 'avo.gemspec', 'Gemfile', 'Gemfile.lock']
33
33
  .reject { |file| file.start_with? 'app/frontend' }
34
34
 
35
- spec.add_dependency 'rails', '~> 6.0.2', '>= 6.0.2.1'
35
+ spec.add_dependency 'rails', '>= 6.0'
36
36
  spec.add_dependency 'kaminari'
37
37
  spec.add_dependency 'zeitwerk'
38
38
  spec.add_dependency 'inline_svg'
39
39
  spec.add_dependency 'webpacker'
40
40
  spec.add_dependency 'countries'
41
+ spec.add_dependency 'pundit'
42
+ spec.add_dependency 'httparty'
41
43
  end
@@ -0,0 +1 @@
1
+ u5m9Je6F3j/LCnOwcN7cXW37M63Bs0KNZC1fHRObWx4YQkV9DJl1k9H9+mlvV3irAC7NZB9H0BExhxquReVe2N0v6s3E3EvOWY8wfZhFONJpK8V0qerYJIhJvCOOPv0hfS/HQdcvUQx/i1faXEgr8QVR7nGVBHUzulwgab0lUBSAV0jjEbT6o+amTojFudelgtw0zmlZW9JL7OZ/IwZ7zxbi8/yoWB52lU4VKOxZV7AEWAVDzL/0ZogLRG12BsTSmqC0jS5ocdnOgV0X7oNeYL9HINqLLM5suBB+BOf3xL6vgngu7UDRAAQ/mN2TDGsvklF8bwVz0dDVQ0RuvdeQ9TWEeM75hhzGKG6wPKBjh4ebrKp7g9TT76F6omLe/hG30MbohGxaLVHIJjyJixfKlLlYwdNWeOjXwYsn--7CEWrUIlraWa8R0S--ia1zVj/2AXISMmbinw6S7g==
@@ -1,20 +1,24 @@
1
1
  Avo::Engine.routes.draw do
2
2
  root 'home#index'
3
3
 
4
- get '/avo-api/search', to: 'resources#search'
5
- get '/avo-api/:resource_name/search', to: 'resources#search'
6
- get '/avo-api/:resource_name', to: 'resources#index'
7
- get '/avo-api/:resource_name/filters', to: 'resources#filters'
4
+ get '/avo-api/:resource_name/filters', to: 'filters#index'
5
+
8
6
  get '/avo-api/:resource_name/actions', to: 'actions#index'
9
7
  post '/avo-api/:resource_name/actions', to: 'actions#handle'
8
+
9
+ get '/avo-api/search', to: 'search#index'
10
+ get '/avo-api/:resource_name/search', to: 'search#resource'
11
+
12
+ get '/avo-api/:resource_name', to: 'resources#index'
10
13
  post '/avo-api/:resource_name', to: 'resources#create'
11
- get '/avo-api/:resource_name/fields', to: 'resources#fields'
14
+ get '/avo-api/:resource_name/new', to: 'resources#new'
12
15
  get '/avo-api/:resource_name/:id', to: 'resources#show'
13
16
  get '/avo-api/:resource_name/:id/edit', to: 'resources#edit'
14
17
  put '/avo-api/:resource_name/:id', to: 'resources#update'
15
18
  delete '/avo-api/:resource_name/:id', to: 'resources#destroy'
16
- post '/avo-api/:resource_name/:id/attach/:attachment_name/:attachment_id', to: 'resources#attach'
17
- post '/avo-api/:resource_name/:id/detach/:attachment_name/:attachment_id', to: 'resources#detach'
19
+
20
+ post '/avo-api/:resource_name/:id/attach/:attachment_name/:attachment_id', to: 'relations#attach'
21
+ post '/avo-api/:resource_name/:id/detach/:attachment_name/:attachment_id', to: 'relations#detach'
18
22
 
19
23
  # Tools
20
24
  get '/avo-tools/resource-overview', to: 'resource_overview#index'
data/lib/avo.rb CHANGED
@@ -11,6 +11,8 @@ require_relative 'avo/app/filters/select_filter'
11
11
 
12
12
  require_relative 'avo/app/resource'
13
13
 
14
+ require_relative 'avo/app/licensing/license_manager'
15
+
14
16
  module Avo
15
17
  ROOT_PATH = Pathname.new(File.join(__dir__, '..'))
16
18
 
@@ -4,37 +4,35 @@ require_relative 'filters/select_filter'
4
4
  require_relative 'filters/boolean_filter'
5
5
  require_relative 'resource'
6
6
  require_relative 'tool'
7
+ require_relative 'authorization_service'
7
8
 
8
9
  module Avo
9
10
  class App
10
11
  @@app = {
11
12
  root_path: '',
12
- tools: [],
13
- tool_classes: [],
14
13
  resources: [],
15
14
  field_names: {},
16
15
  }
16
+ @@license = nil
17
17
 
18
18
  class << self
19
- def init
19
+ def boot
20
20
  @@app[:root_path] = Pathname.new(File.join(__dir__, '..', '..'))
21
- # get_tools
22
- # init_tools
23
21
  init_fields
22
+ end
23
+
24
+ def init(current_request = nil)
24
25
  init_resources
26
+ @@license = LicenseManager.new(HQ.new(current_request).response).license
25
27
  end
26
28
 
27
29
  def app
28
30
  @@app
29
31
  end
30
32
 
31
- # def tools
32
- # @@app[:tools]
33
- # end
34
-
35
- # def get_tools
36
- # @@app[:tool_classes] = ToolsManager.get_tools
37
- # end
33
+ def license
34
+ @@license
35
+ end
38
36
 
39
37
  # This method will take all fields available in the Avo::Fields namespace and create a method for them.
40
38
  #
@@ -129,23 +127,14 @@ module Avo
129
127
  name.to_s.camelize.singularize
130
128
  end
131
129
 
132
- # def init_tools
133
- # @@app[:tool_classes].each do |tool_class|
134
- # @@app[:tools].push tool_class.new
135
- # end
136
- # end
137
-
138
- # def render_navigation
139
- # navigation = []
140
- # @@app[:tools].each do |tool|
141
- # navigation.push(tool.render_navigation) if tool.class.method_defined?(:render_navigation)
142
- # end
143
-
144
- # navigation.join('')
145
- # end
146
-
147
- def get_resources_navigation
148
- App.get_resources.map { |resource| { label: resource.resource_name_plural.humanize, resource_name: resource.url.pluralize } }.to_json.to_s.html_safe
130
+ def get_resources_navigation(user)
131
+ App.get_resources
132
+ .select { |resource| AuthorizationService::authorize user, resource.model, Avo.configuration.authorization_methods.stringify_keys['index'] }
133
+ .map { |resource| { label: resource.resource_name_plural.humanize, resource_name: resource.url.pluralize } }
134
+ .reject { |i| i.blank? }
135
+ .to_json
136
+ .to_s
137
+ .html_safe
149
138
  end
150
139
  end
151
140
  end
@@ -0,0 +1,40 @@
1
+ module Avo
2
+ class AuthorizationService
3
+ class << self
4
+ def authorize(user, record, action)
5
+ return true if skip_authorization
6
+
7
+ begin
8
+ if Pundit.policy user, record
9
+ Pundit.authorize user, record, action
10
+ end
11
+ true
12
+ rescue Pundit::NotAuthorizedError => error
13
+ false
14
+ end
15
+ end
16
+
17
+ def authorize_action(user, record, action)
18
+ action = Avo.configuration.authorization_methods.stringify_keys[action.to_s]
19
+
20
+ return true if action.nil?
21
+
22
+ authorize user, record, action
23
+ end
24
+
25
+ def with_policy(user, model)
26
+ return model if skip_authorization
27
+
28
+ begin
29
+ Pundit.policy_scope! user, model
30
+ rescue => exception
31
+ model
32
+ end
33
+ end
34
+
35
+ def skip_authorization
36
+ Avo::App.license.lacks :authorization
37
+ end
38
+ end
39
+ end
40
+ end
@@ -27,6 +27,7 @@ module Avo
27
27
  fields[:relation_class] = target_resource.class.to_s
28
28
  fields[:path] = target_resource.url
29
29
  fields[:relationship] = :has_and_belongs_to_many
30
+ fields[:relationship_model] = target_resource.model.name
30
31
 
31
32
  fields
32
33
  end
@@ -27,6 +27,7 @@ module Avo
27
27
  fields[:relation_class] = target_resource.class.to_s
28
28
  fields[:path] = target_resource.url
29
29
  fields[:relationship] = :has_many
30
+ fields[:relationship_model] = target_resource.model.name
30
31
 
31
32
  fields
32
33
  end
@@ -1,14 +1,14 @@
1
1
  module Avo
2
2
  module Fields
3
3
  class IdField < Field
4
- DEFAULT_VALUE = 'id'
5
-
6
4
  def initialize(name, **args, &block)
5
+ default_value = 'id'
6
+
7
7
  if name.nil?
8
- @name = name = DEFAULT_VALUE
8
+ @name = name = default_value
9
9
  elsif !name.is_a? String and !name.is_a? Symbol
10
10
  args_copy = name
11
- @name = name = DEFAULT_VALUE
11
+ @name = name = default_value
12
12
  args = args_copy
13
13
  end
14
14
 
@@ -0,0 +1,4 @@
1
+ module Avo
2
+ class CommunityLicense < License
3
+ end
4
+ end
@@ -0,0 +1,85 @@
1
+ module Avo
2
+ class HQ
3
+ attr_accessor :current_request
4
+
5
+ ENDPOINT = 'https://avohq.io/api/v1/licenses/check'
6
+ CACHE_KEY = 'avo.hq.response'
7
+ REQUEST_TIMEOUT = 5 # seconds
8
+
9
+ def initialize(current_request)
10
+ @current_request = current_request
11
+ end
12
+
13
+ def response
14
+ @hq_response or request
15
+ end
16
+
17
+ private
18
+ def request
19
+ return cached_response if has_cached_response
20
+
21
+ begin
22
+ perform_and_cache_request
23
+ rescue HTTParty::Error => exception
24
+ cache_and_return_error 'HTTP client error.', exception.message
25
+ rescue Net::OpenTimeout => exception
26
+ cache_and_return_error 'Request timeout.', exception.message
27
+ rescue SocketError => exception
28
+ cache_and_return_error 'Connection error.', exception.message
29
+ end
30
+ end
31
+
32
+ def perform_and_cache_request
33
+ hq_response = perform_request
34
+
35
+ return cache_and_return_error 'Avo HQ Internal server error.', hq_response.body if hq_response.code == 500
36
+
37
+ cache_response 1.hour.to_i, hq_response.parsed_response if hq_response.code == 200
38
+ end
39
+
40
+ def cache_response(time, response)
41
+ response.merge!(
42
+ expiry: time,
43
+ **payload,
44
+ ).stringify_keys!
45
+
46
+ Rails.cache.write(CACHE_KEY, response, expires_in: time)
47
+
48
+ @hq_response = response
49
+
50
+ response
51
+ end
52
+
53
+ def perform_request
54
+ puts 'Performing request to avohq.io API to check license availability.'.inspect if Rails.env.development?
55
+
56
+ HTTParty.post ENDPOINT, body: payload.to_json, headers: { 'Content-type': 'application/json' }, timeout: REQUEST_TIMEOUT
57
+ end
58
+
59
+ def payload
60
+ {
61
+ license: Avo.configuration.license,
62
+ license_key: Avo.configuration.license_key,
63
+ avo_version: Avo::VERSION,
64
+ rails_version: Rails::VERSION::STRING,
65
+ ruby_version: RUBY_VERSION,
66
+ environment: Rails.env,
67
+ ip: current_request.ip,
68
+ host: current_request.host,
69
+ port: current_request.port,
70
+ }
71
+ end
72
+
73
+ def cache_and_return_error(error, exception_message = '')
74
+ cache_response 5.minutes.to_i, { error: error, exception_message: exception_message }.stringify_keys
75
+ end
76
+
77
+ def has_cached_response
78
+ Rails.cache.exist? CACHE_KEY
79
+ end
80
+
81
+ def cached_response
82
+ Rails.cache.read CACHE_KEY
83
+ end
84
+ end
85
+ end