tiny_admin 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +171 -0
- data/lib/tiny_admin/actions/basic_action.rb +17 -0
- data/lib/tiny_admin/actions/index.rb +48 -0
- data/lib/tiny_admin/actions/show.rb +23 -0
- data/lib/tiny_admin/authentication.rb +36 -0
- data/lib/tiny_admin/basic_app.rb +30 -0
- data/lib/tiny_admin/context.rb +9 -0
- data/lib/tiny_admin/field.rb +20 -0
- data/lib/tiny_admin/plugins/active_record_repository.rb +83 -0
- data/lib/tiny_admin/plugins/base_repository.rb +15 -0
- data/lib/tiny_admin/plugins/no_auth.rb +25 -0
- data/lib/tiny_admin/plugins/simple_auth.rb +44 -0
- data/lib/tiny_admin/router.rb +125 -0
- data/lib/tiny_admin/settings.rb +83 -0
- data/lib/tiny_admin/utils.rb +40 -0
- data/lib/tiny_admin/version.rb +5 -0
- data/lib/tiny_admin/views/actions/index.rb +106 -0
- data/lib/tiny_admin/views/actions/show.rb +57 -0
- data/lib/tiny_admin/views/components/filters_form.rb +57 -0
- data/lib/tiny_admin/views/components/flash.rb +25 -0
- data/lib/tiny_admin/views/components/head.rb +31 -0
- data/lib/tiny_admin/views/components/navbar.rb +42 -0
- data/lib/tiny_admin/views/components/pagination.rb +33 -0
- data/lib/tiny_admin/views/default_layout.rb +101 -0
- data/lib/tiny_admin/views/pages/page_not_found.rb +21 -0
- data/lib/tiny_admin/views/pages/record_not_found.rb +21 -0
- data/lib/tiny_admin/views/pages/root.rb +17 -0
- data/lib/tiny_admin/views/pages/simple_auth_login.rb +32 -0
- data/lib/tiny_admin.rb +26 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: afea7b433e47bdf47e2682a334e2ffe7761e6e341aec9b43746291faf4d16406
|
4
|
+
data.tar.gz: 6c53fbae6a6c6749ef423bbd190094c25149abae29c6c235bf49f13fb3d6906a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b9701bccac2aa19c6fb838c037e0522d6ee8d3560ec1d5aa743725a64d1c2e4bfc7369faaef9bdc6b98da280f8f1d82c302fadfae5620fe1e958857238005c4
|
7
|
+
data.tar.gz: 16763a9cdf6df77cc261f5a5493803a55cd41756888026fb93ccdf66bf7c6fe7412482b39a666fd026892787addeb55c80adef45b1af479b0f062eb7276aaa53
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2023 Mattia Roccoberton
|
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,171 @@
|
|
1
|
+
# Tiny Admin
|
2
|
+
|
3
|
+
A compact and composible dashboard component for Ruby.
|
4
|
+
|
5
|
+
The main features are:
|
6
|
+
- a Rack app that can be mounted in any Rack-enabled framework;
|
7
|
+
- some features are handled as plugins, so they can be replaced with little effort;
|
8
|
+
- routing is provided by Roda (which is small and performant);
|
9
|
+
- views are Phlex components.
|
10
|
+
|
11
|
+
See (extra)[extra] folder for usage examples.
|
12
|
+
|
13
|
+
Please ⭐ if you like it.
|
14
|
+
|
15
|
+
## Install
|
16
|
+
|
17
|
+
- Add to your Gemfile: `gem 'tiny_admin'`
|
18
|
+
- For a Rails project: add an initializer and the YAML config - see (configuration)[#configuration] below.
|
19
|
+
|
20
|
+
## Plugins and components
|
21
|
+
|
22
|
+
Every plugin or component can be replaced.
|
23
|
+
|
24
|
+
### Authentication
|
25
|
+
|
26
|
+
There are 2 plugins included:
|
27
|
+
- _SimpleAuth_: provides a simple session authentication based on Warden (`warden` gem must be included in the host project);
|
28
|
+
- _NoAuth_: no authentication.
|
29
|
+
|
30
|
+
### Repository
|
31
|
+
|
32
|
+
There is 1 plugin included:
|
33
|
+
- _ActiveRecordRepository_: isolates the query layer to expose the resources in the admin interface.
|
34
|
+
|
35
|
+
### View pages
|
36
|
+
|
37
|
+
There are 5 view pages included:
|
38
|
+
- _Root_: define how to present the content in the main page of the interface;
|
39
|
+
- _PageNotFound_: define how to present pages not found;
|
40
|
+
- _RecordNotFound_: define how to present record not found page;
|
41
|
+
- _SimpleAuthLogin_: define how to present the login form for SimpleAuth plugin;
|
42
|
+
- _Index_: define how to present a collection of items;
|
43
|
+
- _Show_: define how to present the details of an item.
|
44
|
+
|
45
|
+
### View components
|
46
|
+
|
47
|
+
There are 5 view components included:
|
48
|
+
- _FiltersForm_: define how to present the filters form in the resource collection pages;
|
49
|
+
- _Flash_: define how to present the flash messages;
|
50
|
+
- _Head_: define how to present the Head tag;
|
51
|
+
- _Navbar_: define how to present the navbar (the default one uses the Bootstrap structure);
|
52
|
+
- _Pagination_: define how to present the pagination of a collection.
|
53
|
+
|
54
|
+
## Configuration
|
55
|
+
|
56
|
+
TinyAdmin can be configured programmatically or using a YAML config.
|
57
|
+
|
58
|
+
Example:
|
59
|
+
|
60
|
+
```rb
|
61
|
+
# config/initializers/tiny_admin.rb
|
62
|
+
|
63
|
+
# hash generated using: Digest::SHA512.hexdigest("changeme")
|
64
|
+
ENV['ADMIN_PASSWORD_HASH'] = 'f1891cea80fc05e433c943254c6bdabc159577a02a7395dfebbfbc4f7661d4af56f2d372131a45936de40160007368a56ef216a30cb202c66d3145fd24380906'
|
65
|
+
config = Rails.root.join('config/tiny_admin.yml').to_s
|
66
|
+
TinyAdmin.configure_from_file(config)
|
67
|
+
```
|
68
|
+
|
69
|
+
```yml
|
70
|
+
---
|
71
|
+
authentication:
|
72
|
+
plugin: TinyAdmin::Plugins::SimpleAuth
|
73
|
+
page_not_found: Admin::PageNotFound
|
74
|
+
record_not_found: Admin::RecordNotFound
|
75
|
+
root:
|
76
|
+
title: 'Tiny Admin'
|
77
|
+
page: Admin::PageRoot
|
78
|
+
# redirect: posts
|
79
|
+
sections:
|
80
|
+
- slug: google
|
81
|
+
name: Google.it
|
82
|
+
type: url
|
83
|
+
url: https://www.google.it
|
84
|
+
options:
|
85
|
+
target: '_blank'
|
86
|
+
- slug: stats
|
87
|
+
name: Stats
|
88
|
+
type: page
|
89
|
+
page: Admin::Stats
|
90
|
+
- slug: authors
|
91
|
+
name: Authors
|
92
|
+
type: resource
|
93
|
+
model: Author
|
94
|
+
repository: Admin::AuthorsRepo
|
95
|
+
collection_actions:
|
96
|
+
- latests: Admin::LatestAuthorsAction
|
97
|
+
member_actions:
|
98
|
+
- csv_export: Admin::CsvExportAuthorAction
|
99
|
+
# only:
|
100
|
+
# - index
|
101
|
+
# options:
|
102
|
+
# - hidden
|
103
|
+
- slug: posts
|
104
|
+
name: Posts
|
105
|
+
type: resource
|
106
|
+
model: Post
|
107
|
+
index:
|
108
|
+
sort:
|
109
|
+
- author_id DESC
|
110
|
+
pagination: 15
|
111
|
+
attributes:
|
112
|
+
- id
|
113
|
+
- title
|
114
|
+
- field: author_id
|
115
|
+
link_to: authors
|
116
|
+
- state
|
117
|
+
- published
|
118
|
+
- dt
|
119
|
+
- field: created_at
|
120
|
+
converter: Admin::Utils
|
121
|
+
method: datetime_formatter
|
122
|
+
filters:
|
123
|
+
- title
|
124
|
+
- field: state
|
125
|
+
type: select
|
126
|
+
values:
|
127
|
+
- available
|
128
|
+
- unavailable
|
129
|
+
- arriving
|
130
|
+
- published
|
131
|
+
show:
|
132
|
+
attributes:
|
133
|
+
- id
|
134
|
+
- title
|
135
|
+
- description
|
136
|
+
- field: author_id
|
137
|
+
link_to: authors
|
138
|
+
- category
|
139
|
+
- published
|
140
|
+
- state
|
141
|
+
- created_at
|
142
|
+
style_links:
|
143
|
+
- href: /bootstrap.min.css
|
144
|
+
rel: stylesheet
|
145
|
+
scripts:
|
146
|
+
- src: /bootstrap.bundle.min.js
|
147
|
+
extra_styles: >
|
148
|
+
.navbar {
|
149
|
+
background-color: var(--bs-cyan);
|
150
|
+
}
|
151
|
+
.main-content {
|
152
|
+
background-color: var(--bs-gray-100);
|
153
|
+
}
|
154
|
+
.main-content a {
|
155
|
+
text-decoration: none;
|
156
|
+
}
|
157
|
+
```
|
158
|
+
|
159
|
+
## Do you like it? Star it!
|
160
|
+
|
161
|
+
If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
|
162
|
+
|
163
|
+
Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).
|
164
|
+
|
165
|
+
## Contributors
|
166
|
+
|
167
|
+
- [Mattia Roccoberton](https://blocknot.es/): author
|
168
|
+
|
169
|
+
## License
|
170
|
+
|
171
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Actions
|
5
|
+
class BasicAction
|
6
|
+
include Utils
|
7
|
+
|
8
|
+
attr_reader :params, :path, :repository
|
9
|
+
|
10
|
+
def initialize(repository, path:, params:)
|
11
|
+
@repository = repository
|
12
|
+
@path = path
|
13
|
+
@params = params
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Actions
|
5
|
+
class Index < BasicAction
|
6
|
+
attr_reader :current_page, :fields_options, :filters_list, :pagination, :query_string, :sort
|
7
|
+
|
8
|
+
def call(app:, context:, options:, actions:)
|
9
|
+
evaluate_options(options)
|
10
|
+
fields = repository.fields(options: fields_options)
|
11
|
+
filters = prepare_filters(fields, filters_list)
|
12
|
+
records, total_count = repository.list(page: current_page, limit: pagination, filters: filters, sort: sort)
|
13
|
+
prepare_record = ->(record) { repository.index_record_attrs(record, fields: fields_options) }
|
14
|
+
title = repository.index_title
|
15
|
+
pages = (total_count / pagination) + 1
|
16
|
+
|
17
|
+
prepare_page(Views::Actions::Index, title: title, context: context, query_string: query_string) do |page|
|
18
|
+
page.setup_pagination(current_page: current_page, pages: pages > 1 ? pages : false)
|
19
|
+
page.setup_records(records: records, fields: fields, prepare_record: prepare_record)
|
20
|
+
page.actions = actions
|
21
|
+
page.filters = filters
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def evaluate_options(options)
|
28
|
+
@fields_options = options[:attributes]&.each_with_object({}) do |field, result|
|
29
|
+
result.merge!(field.is_a?(Hash) ? { field[:field] => field } : { field => { field: field } })
|
30
|
+
end
|
31
|
+
@filters_list = options[:filters]
|
32
|
+
@pagination = options[:pagination] || 10
|
33
|
+
@sort = options[:sort] || ['id']
|
34
|
+
|
35
|
+
@current_page = (params['p'] || 1).to_i
|
36
|
+
@query_string = params_to_s(params.reject { |k, _v| k == 'p' })
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepare_filters(fields, filters_list)
|
40
|
+
filters = (filters_list || []).map { _1.is_a?(Hash) ? _1 : { field: _1 } }.index_by { _1[:field] }
|
41
|
+
values = (params['q'] || {})
|
42
|
+
fields.each_with_object({}) do |field, result|
|
43
|
+
result[field] = { value: values[field.name], filter: filters[field.name] } if filters.key?(field.name)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Actions
|
5
|
+
class Show < BasicAction
|
6
|
+
def call(app:, context:, options:, actions:)
|
7
|
+
fields_options = (options[:attributes] || []).each_with_object({}) do |field, result|
|
8
|
+
result.merge!(field.is_a?(Hash) ? { field[:field] => field } : { field => { field: field } })
|
9
|
+
end
|
10
|
+
record = repository.find(context.reference)
|
11
|
+
prepare_record = ->(record_data) { repository.show_record_attrs(record_data, fields: fields_options) }
|
12
|
+
fields = repository.fields(options: fields_options)
|
13
|
+
|
14
|
+
prepare_page(Views::Actions::Show, title: repository.show_title(record), context: context) do |page|
|
15
|
+
page.setup_record(record: record, fields: fields, prepare_record: prepare_record)
|
16
|
+
page.actions = actions
|
17
|
+
end
|
18
|
+
rescue Plugins::BaseRepository::RecordNotFound => _e
|
19
|
+
prepare_page(options[:record_not_found_page] || Views::Pages::RecordNotFound)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
class Authentication < BasicApp
|
5
|
+
route do |r|
|
6
|
+
r.get 'unauthenticated' do
|
7
|
+
if current_user
|
8
|
+
r.redirect settings.root_path
|
9
|
+
else
|
10
|
+
render_login
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
r.post 'unauthenticated' do
|
15
|
+
render_login(warnings: ['Failed to authenticate'])
|
16
|
+
end
|
17
|
+
|
18
|
+
r.get 'logout' do
|
19
|
+
logout_user
|
20
|
+
r.redirect settings.root_path
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def render_login(notices: nil, warnings: nil, errors: nil)
|
27
|
+
page = prepare_page(settings.authentication[:login], options: %i[no_menu compact_layout])
|
28
|
+
page.setup_flash_messages(
|
29
|
+
notices: notices || flash['notices'],
|
30
|
+
warnings: warnings || flash['warnings'],
|
31
|
+
errors: errors || flash['errors']
|
32
|
+
)
|
33
|
+
render(inline: page.call)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
class BasicApp < Roda
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def authentication_plugin
|
9
|
+
plugin = TinyAdmin::Settings.instance.authentication&.dig(:plugin)
|
10
|
+
plugin_class = plugin.is_a?(String) ? Object.const_get(plugin) : plugin
|
11
|
+
plugin_class || TinyAdmin::Plugins::NoAuth
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
plugin :flash
|
16
|
+
plugin :not_found
|
17
|
+
plugin :render, engine: 'html'
|
18
|
+
plugin :sessions, secret: SecureRandom.hex(64)
|
19
|
+
|
20
|
+
plugin authentication_plugin
|
21
|
+
|
22
|
+
not_found { prepare_page(settings.page_not_found).call }
|
23
|
+
|
24
|
+
def attach_flash_messages(page)
|
25
|
+
return unless page.respond_to?(:setup_flash_messages)
|
26
|
+
|
27
|
+
page.setup_flash_messages(notices: flash['notices'], warnings: flash['warnings'], errors: flash['errors'])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
class Field
|
5
|
+
attr_reader :name, :options, :title, :type
|
6
|
+
|
7
|
+
def initialize(type:, name:, title:, options: {})
|
8
|
+
@type = type
|
9
|
+
@name = name
|
10
|
+
@title = title || name
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def create_field(name:, title:, type: nil, options: {})
|
16
|
+
new(type: type, name: name, title: title, options: options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Plugins
|
5
|
+
class ActiveRecordRepository < BaseRepository
|
6
|
+
def index_record_attrs(record, fields: nil)
|
7
|
+
return record.attributes.transform_values(&:to_s) if !fields || fields.empty?
|
8
|
+
|
9
|
+
record.attributes.slice(*fields.keys).each_with_object({}) do |(key, value), result|
|
10
|
+
field_data = fields[key]
|
11
|
+
result[key] =
|
12
|
+
if field_data[:converter] && field_data[:method]
|
13
|
+
converter = Object.const_get(field_data[:converter])
|
14
|
+
converter.send(field_data[:method], value)
|
15
|
+
else
|
16
|
+
value&.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def index_title
|
22
|
+
title = model.to_s
|
23
|
+
title.respond_to?(:pluralize) ? title.pluralize : title
|
24
|
+
end
|
25
|
+
|
26
|
+
def fields(options: nil)
|
27
|
+
opts = options || {}
|
28
|
+
columns = model.columns
|
29
|
+
if !opts.empty?
|
30
|
+
extra_fields = opts.keys - model.column_names
|
31
|
+
raise "Some requested fields are not available: #{extra_fields.join(', ')}" if extra_fields.any?
|
32
|
+
|
33
|
+
columns = opts.keys.map { |field| columns.find { _1.name == field } }
|
34
|
+
end
|
35
|
+
columns.map do |column|
|
36
|
+
name = column.name
|
37
|
+
type = opts.dig(column.name, :type) || column.type
|
38
|
+
TinyAdmin::Field.create_field(name: name, title: name.humanize, type: type, options: opts[name])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def show_record_attrs(record, fields: nil)
|
43
|
+
attrs = !fields || fields.empty? ? record.attributes : record.attributes.slice(*fields.keys)
|
44
|
+
attrs.transform_values(&:to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
def show_title(record)
|
48
|
+
"#{model} ##{record.id}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def find(reference)
|
52
|
+
model.find(reference)
|
53
|
+
rescue ActiveRecord::RecordNotFound => e
|
54
|
+
raise BaseRepository::RecordNotFound, e.message
|
55
|
+
end
|
56
|
+
|
57
|
+
def list(page: 1, limit: 10, filters: nil, sort: ['id'])
|
58
|
+
page_offset = page.positive? ? (page - 1) * limit : 0
|
59
|
+
query = model.all.order(sort)
|
60
|
+
query = apply_filters(query, filters) if filters
|
61
|
+
records = query.offset(page_offset).limit(limit).to_a
|
62
|
+
[records, query.count]
|
63
|
+
end
|
64
|
+
|
65
|
+
def apply_filters(query, filters)
|
66
|
+
filters.each do |field, filter|
|
67
|
+
value = filter&.dig(:value)
|
68
|
+
next if value.nil? || value == ''
|
69
|
+
|
70
|
+
query =
|
71
|
+
case field.type
|
72
|
+
when :string
|
73
|
+
value = ActiveRecord::Base.sanitize_sql_like(value.strip)
|
74
|
+
query.where("#{field.name} LIKE ?", "%#{value}%")
|
75
|
+
else
|
76
|
+
query.where(field.name => value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
query
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Plugins
|
5
|
+
module NoAuth
|
6
|
+
class << self
|
7
|
+
def configure(_app, _opts = {}); end
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
def authenticate_user!
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_user
|
16
|
+
'admin'
|
17
|
+
end
|
18
|
+
|
19
|
+
def logout_user
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module TinyAdmin
|
6
|
+
module Plugins
|
7
|
+
module SimpleAuth
|
8
|
+
class << self
|
9
|
+
def configure(app, opts = {})
|
10
|
+
Warden::Strategies.add(:secret) do
|
11
|
+
def authenticate!
|
12
|
+
hash = ENV.fetch('ADMIN_PASSWORD_HASH')
|
13
|
+
secret = params['secret']
|
14
|
+
return fail(:invalid_credentials) if !secret || Digest::SHA512.hexdigest(secret) != hash
|
15
|
+
|
16
|
+
success!(app: 'TinyAdmin')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
app.opts[:login_form] = opts[:login_form] || TinyAdmin::Views::Pages::SimpleAuthLogin
|
21
|
+
|
22
|
+
app.use Warden::Manager do |manager|
|
23
|
+
manager.default_strategies :secret
|
24
|
+
manager.failure_app = TinyAdmin::Authentication
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
def authenticate_user!
|
31
|
+
env['warden'].authenticate!
|
32
|
+
end
|
33
|
+
|
34
|
+
def current_user
|
35
|
+
env['warden'].user
|
36
|
+
end
|
37
|
+
|
38
|
+
def logout_user
|
39
|
+
env['warden'].logout
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
class Router < BasicApp
|
5
|
+
TinyAdmin::Settings.instance.load_settings
|
6
|
+
|
7
|
+
route do |r|
|
8
|
+
r.on 'auth' do
|
9
|
+
context.slug = nil
|
10
|
+
r.run Authentication
|
11
|
+
end
|
12
|
+
|
13
|
+
authenticate_user!
|
14
|
+
|
15
|
+
r.root do
|
16
|
+
root_route(r)
|
17
|
+
end
|
18
|
+
|
19
|
+
r.post '' do
|
20
|
+
context.slug = nil
|
21
|
+
r.redirect settings.root_path
|
22
|
+
end
|
23
|
+
|
24
|
+
TinyAdmin::Settings.instance.pages.each do |slug, data|
|
25
|
+
setup_page_route(r, slug, data)
|
26
|
+
end
|
27
|
+
|
28
|
+
TinyAdmin::Settings.instance.resources.each do |slug, options|
|
29
|
+
setup_resource_routes(r, slug, options: options || {})
|
30
|
+
end
|
31
|
+
|
32
|
+
nil # NOTE: needed to skip the last line (each) return value
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def render_page(page)
|
38
|
+
attach_flash_messages(page)
|
39
|
+
render(inline: page.call)
|
40
|
+
end
|
41
|
+
|
42
|
+
def root_route(router)
|
43
|
+
context.slug = nil
|
44
|
+
if settings.root[:redirect]
|
45
|
+
router.redirect "#{settings.root_path}/#{settings.root[:redirect]}"
|
46
|
+
else
|
47
|
+
page = settings.root[:page]
|
48
|
+
page_class = page.is_a?(String) ? Object.const_get(page) : page
|
49
|
+
render_page prepare_page(page_class, context: context)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_page_route(router, slug, page_class)
|
54
|
+
router.get slug do
|
55
|
+
context.slug = slug
|
56
|
+
render_page prepare_page(page_class, context: context)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_resource_routes(router, slug, options:)
|
61
|
+
router.on slug do
|
62
|
+
context.slug = slug
|
63
|
+
setup_collection_routes(router, slug: slug, options: options)
|
64
|
+
setup_member_routes(router, slug: slug, options: options)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def setup_collection_routes(router, slug:, options:)
|
69
|
+
repository = options[:repository].new(options[:model])
|
70
|
+
index_options = options[:index] || {}
|
71
|
+
custom_actions = []
|
72
|
+
|
73
|
+
# Custom actions
|
74
|
+
(options[:collection_actions] || []).each do |custom_action|
|
75
|
+
action_slug, action = custom_action.first
|
76
|
+
action_class = action.is_a?(String) ? Object.const_get(action) : action
|
77
|
+
custom_actions << action_slug.to_s
|
78
|
+
router.get action_slug.to_s do
|
79
|
+
custom_action = action_class.new(repository, path: request.path, params: request.params)
|
80
|
+
render_page custom_action.call(app: self, context: context, options: index_options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Index
|
85
|
+
actions = options[:only]
|
86
|
+
if !actions || actions.include?(:index) || actions.include?('index')
|
87
|
+
router.is do
|
88
|
+
index_action = TinyAdmin::Actions::Index.new(repository, path: request.path, params: request.params)
|
89
|
+
render_page index_action.call(app: self, context: context, options: index_options, actions: custom_actions)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def setup_member_routes(router, slug:, options:)
|
95
|
+
repository = options[:repository].new(options[:model])
|
96
|
+
show_options = (options[:show] || {}).merge(record_not_found_page: settings.record_not_found)
|
97
|
+
custom_actions = []
|
98
|
+
|
99
|
+
router.on String do |reference|
|
100
|
+
context.reference = reference
|
101
|
+
|
102
|
+
# Custom actions
|
103
|
+
(options[:member_actions] || []).each do |custom_action|
|
104
|
+
action_slug, action = custom_action.first
|
105
|
+
action_class = action.is_a?(String) ? Object.const_get(action) : action
|
106
|
+
custom_actions << action_slug.to_s
|
107
|
+
|
108
|
+
router.get action_slug.to_s do
|
109
|
+
custom_action = action_class.new(repository, path: request.path, params: request.params)
|
110
|
+
render_page custom_action.call(app: self, context: context, options: show_options)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Show
|
115
|
+
actions = options[:only]
|
116
|
+
if !actions || actions.include?(:show) || actions.include?('show')
|
117
|
+
router.is do
|
118
|
+
show_action = TinyAdmin::Actions::Show.new(repository, path: request.path, params: request.params)
|
119
|
+
render_page show_action.call(app: self, context: context, options: show_options, actions: custom_actions)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|