tiny_admin 0.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.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
|