roda-kabk 0.1.5
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.
Potentially problematic release.
This version of roda-kabk might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +88 -0
- data/LICENSE +21 -0
- data/README.md +188 -0
- data/TUTORIAL.md +227 -0
- data/examples/app.rb +128 -0
- data/examples/config.ru +2 -0
- data/lib/roda/plugins/kabk/route_tree.rb +314 -0
- data/lib/roda/plugins/kabk.rb +41 -0
- data/public/assets/scripts.js +8638 -0
- data/public/assets/style.css +1 -0
- data/public/index.html +119 -0
- data/public/simurgh-logo.svg +33 -0
- data/roda-kabk.svg +118 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 29d71b61867e52a48284e539f00194c7094a5a6b3c80282f0a4ed6702085132b
|
|
4
|
+
data.tar.gz: 3432f771d7e78f564bf86bf5be72e636725b92185180940d1cce07e71fdfeb8a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8a0ebee193374a27891b1416081a3fd60c2039eab8f8d1886fe579676da0787105efbe4921dcd0ee6606d77c6bfe04cee1e3e352ddbd57adfa7da15b613cf1fd
|
|
7
|
+
data.tar.gz: 0d3510f11f3becae6b999b1a8a5e2d9458b553d971877e527867625c2038da8746b0de3cc388acd05a5d9f7587e6a9507fa1d1dea191658a4cb06c0ec63e49a4
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
roda-kabk (0.1.2)
|
|
5
|
+
kabk (~> 0.1.2)
|
|
6
|
+
roda (~> 3.0)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
base64 (0.3.0)
|
|
12
|
+
bigdecimal (4.1.2)
|
|
13
|
+
concurrent-ruby (1.3.8)
|
|
14
|
+
csv (3.3.6)
|
|
15
|
+
diff-lcs (1.6.2)
|
|
16
|
+
jwt (2.10.3)
|
|
17
|
+
base64
|
|
18
|
+
kabk (0.1.2)
|
|
19
|
+
concurrent-ruby (~> 1.1)
|
|
20
|
+
csv
|
|
21
|
+
jwt (~> 2.7)
|
|
22
|
+
sequel (~> 5.0)
|
|
23
|
+
logger (1.7.0)
|
|
24
|
+
nio4r (2.7.5)
|
|
25
|
+
puma (8.0.2)
|
|
26
|
+
nio4r (~> 2.0)
|
|
27
|
+
rack (3.2.6)
|
|
28
|
+
rack-test (2.2.0)
|
|
29
|
+
rack (>= 1.3)
|
|
30
|
+
rackup (2.3.1)
|
|
31
|
+
rack (>= 3)
|
|
32
|
+
roda (3.106.0)
|
|
33
|
+
rack
|
|
34
|
+
rspec (3.13.2)
|
|
35
|
+
rspec-core (~> 3.13.0)
|
|
36
|
+
rspec-expectations (~> 3.13.0)
|
|
37
|
+
rspec-mocks (~> 3.13.0)
|
|
38
|
+
rspec-core (3.13.6)
|
|
39
|
+
rspec-support (~> 3.13.0)
|
|
40
|
+
rspec-expectations (3.13.5)
|
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
42
|
+
rspec-support (~> 3.13.0)
|
|
43
|
+
rspec-mocks (3.13.8)
|
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
45
|
+
rspec-support (~> 3.13.0)
|
|
46
|
+
rspec-support (3.13.7)
|
|
47
|
+
sequel (5.107.0)
|
|
48
|
+
bigdecimal
|
|
49
|
+
sqlite3 (2.9.5-x64-mingw-ucrt)
|
|
50
|
+
|
|
51
|
+
PLATFORMS
|
|
52
|
+
x64-mingw-ucrt
|
|
53
|
+
|
|
54
|
+
DEPENDENCIES
|
|
55
|
+
logger
|
|
56
|
+
puma
|
|
57
|
+
rack-test
|
|
58
|
+
rackup
|
|
59
|
+
roda-kabk!
|
|
60
|
+
rspec
|
|
61
|
+
sqlite3
|
|
62
|
+
|
|
63
|
+
CHECKSUMS
|
|
64
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
65
|
+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
66
|
+
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
|
|
67
|
+
csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456
|
|
68
|
+
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
69
|
+
jwt (2.10.3) sha256=e4d9352fbc7309b1a7448c7dd713dfe4d8c47077af80759cdbed8f878ea0b484
|
|
70
|
+
kabk (0.1.2)
|
|
71
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
72
|
+
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
|
73
|
+
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
|
|
74
|
+
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
|
|
75
|
+
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
|
76
|
+
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
|
77
|
+
roda (3.106.0) sha256=1a1cfb13c6690db158a3929e9c9ece88e697fb23b05baae63f07b0773e3bbade
|
|
78
|
+
roda-kabk (0.1.2)
|
|
79
|
+
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
80
|
+
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
81
|
+
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
82
|
+
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
|
|
83
|
+
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
|
|
84
|
+
sequel (5.107.0) sha256=019629313205295b007f6df2c5eab19ddc40710d549fd445ac300f2853a370db
|
|
85
|
+
sqlite3 (2.9.5-x64-mingw-ucrt) sha256=b00d5697994ee8589b6096694a2130aa5567db64373baca55ea98c9bf958f46a
|
|
86
|
+
|
|
87
|
+
BUNDLED WITH
|
|
88
|
+
4.0.16
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parham Taheri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="roda-kabk.svg" alt="Roda Kabk Logo" width="300" />
|
|
3
|
+
<br>
|
|
4
|
+
Roda Kabk
|
|
5
|
+
</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/roda-kabk"><img src="https://img.shields.io/gem/v/roda-kabk.svg?color=blue" alt="Gem Version"></a>
|
|
9
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
> **Kabk carries the Ruby to Simorgh**
|
|
13
|
+
|
|
14
|
+
`roda-kabk` is the official [Roda](https://roda.jeremyevans.net/) plugin for [Kabk](https://github.com/par-taheri/Kabk) — seamlessly binding Roda applications to the Simurgh Panel protocol (v1.6.0) for schema-driven dynamic REST APIs without duplicating backend logic.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Embedded Simurgh Panel UI**: Bundles and serves the pre-built single-page admin dashboard SPA directly at `mount_at` (`/api/admin`) with embedded schema injection.
|
|
21
|
+
- **Generic REST Engine**: Auto-generates full CRUD, search, multi-field filtering, sorting, and pagination for registered Sequel models.
|
|
22
|
+
- **Pluggable File Uploads**: Seamless integration with **Shrine**, ActiveStorage, or custom storage handlers via configurable procs.
|
|
23
|
+
- **Authentication & Security**: Integrated JWT authentication (`login`, `refresh`, `me`, `change-password`) and Role-Based Access Control (RBAC).
|
|
24
|
+
- **Optimistic Concurrency Control (OCC)**: Built-in support for preventing concurrent record overwrite conflicts (`updated_at` / `version`).
|
|
25
|
+
- **Data Export**: Exposes CSV and XLSX export endpoints out of the box (`/export`).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Add this line to your application's `Gemfile`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
gem 'roda-kabk'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
And then execute:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bundle install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
require 'roda'
|
|
49
|
+
require 'sequel'
|
|
50
|
+
require 'kabk'
|
|
51
|
+
require 'roda/plugins/kabk'
|
|
52
|
+
|
|
53
|
+
# 1. Setup DB and Sequel Model
|
|
54
|
+
DB = Sequel.sqlite
|
|
55
|
+
DB.create_table :articles do
|
|
56
|
+
primary_key :id
|
|
57
|
+
String :title, null: false
|
|
58
|
+
String :content
|
|
59
|
+
DateTime :updated_at
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Article < Sequel::Model; end
|
|
63
|
+
|
|
64
|
+
# 2. Register Entity with Kabk
|
|
65
|
+
Kabk.register(name: "article", table: Article) do
|
|
66
|
+
title "Articles"
|
|
67
|
+
concurrency_field "updated_at"
|
|
68
|
+
|
|
69
|
+
field :id, type: :number, form_type: :number, primary_key: true
|
|
70
|
+
field :title, type: :string, form_type: :text, required: true
|
|
71
|
+
field :content, type: :string, form_type: :textarea
|
|
72
|
+
field :updated_at, type: :datetime, form_type: :datetime, readonly: true, calendar: :jalali
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 3. Mount Kabk Plugin in Roda
|
|
76
|
+
class App < Roda
|
|
77
|
+
plugin :kabk,
|
|
78
|
+
mount_at: "/api/admin",
|
|
79
|
+
serve_ui: true,
|
|
80
|
+
auth_strategy: Kabk::Auth::JwtStrategy.new(secret: ENV.fetch("JWT_SECRET", "super_secret_key")),
|
|
81
|
+
login_handler: ->(username, password) {
|
|
82
|
+
# Fetch user from DB and verify password
|
|
83
|
+
# return user_data hash if valid, nil otherwise
|
|
84
|
+
user = User.first(email: username)
|
|
85
|
+
return nil unless user && user.authenticate(password)
|
|
86
|
+
{ id: user.id, username: user.email, role: user.role }
|
|
87
|
+
},
|
|
88
|
+
change_password_handler: ->(user_context, old_pw, new_pw) {
|
|
89
|
+
# Verify old password and save new password
|
|
90
|
+
user = User[user_context[:id]]
|
|
91
|
+
raise Kabk::InvalidOldPasswordError.new(fields: { old_password: ["Incorrect password"] }) unless user.authenticate(old_pw)
|
|
92
|
+
user.update(password: new_pw)
|
|
93
|
+
},
|
|
94
|
+
upload_handler: ->(file_param, req) {
|
|
95
|
+
# Integration point for Shrine / ActiveStorage
|
|
96
|
+
{
|
|
97
|
+
url: "/uploads/#{file_param[:filename]}",
|
|
98
|
+
file_name: file_param[:filename],
|
|
99
|
+
size: file_param[:tempfile].size,
|
|
100
|
+
mime_type: file_param[:type]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
route do |r|
|
|
105
|
+
# Exposes Dashboard UI, Auth, CRUD, Export, and Upload endpoints under /api/admin
|
|
106
|
+
r.kabk
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Configuration Reference
|
|
114
|
+
|
|
115
|
+
The `plugin :kabk` method accepts the following options:
|
|
116
|
+
|
|
117
|
+
| Option | Type | Default | Description |
|
|
118
|
+
| :--- | :--- | :--- | :--- |
|
|
119
|
+
| `mount_at` | `String` | `"/api/admin"` | Base URI path under which all Kabk admin endpoints and UI are mounted. |
|
|
120
|
+
| `serve_ui` | `Boolean` | `true` | Serves the bundled Simurgh Panel single-page dashboard UI directly at `mount_at`. |
|
|
121
|
+
| `system_config` | `Hash` / `NilClass` | `nil` | Custom system branding and localization settings (overrides default locale/title). |
|
|
122
|
+
| `auth_strategy` | `Kabk::Auth::JwtStrategy` / `NilClass` | `nil` | Authentication strategy object. Set to `nil` to disable auth for development. |
|
|
123
|
+
| `upload_handler` | `Proc` / `Callable` | `nil` | Custom file upload handler block/callable. Processes uploaded file params. |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## File Upload Integration (Shrine Example)
|
|
128
|
+
|
|
129
|
+
You can connect **Shrine** directly using the `upload_handler` option:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
plugin :kabk,
|
|
133
|
+
mount_at: "/api/admin",
|
|
134
|
+
upload_handler: ->(file_param, req) {
|
|
135
|
+
# file_param contains Rack tempfile params: { tempfile: #<File>, filename: "...", type: "..." }
|
|
136
|
+
uploader = ImageUploader.upload(file_param[:tempfile], :store)
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
url: uploader.url,
|
|
140
|
+
file_name: file_param[:filename],
|
|
141
|
+
size: file_param[:tempfile].size,
|
|
142
|
+
mime_type: file_param[:type]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
If no `upload_handler` is provided and no file is attached, the endpoint returns an `HTTP 400 Bad Request` with error code `BAD_REQUEST`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Endpoints Reference
|
|
152
|
+
|
|
153
|
+
All endpoints are exposed under the configured `mount_at` prefix (e.g. `/api/admin`):
|
|
154
|
+
|
|
155
|
+
### Authentication
|
|
156
|
+
- `POST /api/admin/auth/login` — Authenticates user credentials & returns JWT access/refresh tokens.
|
|
157
|
+
- `POST /api/admin/auth/refresh` — Generates new access token from refresh token.
|
|
158
|
+
- `GET /api/admin/auth/me` — Returns current authenticated user context.
|
|
159
|
+
- `POST /api/admin/auth/logout` — Revokes active session.
|
|
160
|
+
- `POST /api/admin/auth/change-password` — Updates user password (requires `old_password` verification).
|
|
161
|
+
|
|
162
|
+
### Resource Operations (Generic REST)
|
|
163
|
+
For any registered entity `plural_name` (e.g., `/articles`, `/users`):
|
|
164
|
+
- `GET /api/admin/:plural_name` — List records (supports `page`, `per_page`, `sort`, `search`, `filter[field]`).
|
|
165
|
+
- `GET /api/admin/:plural_name/:id` — Retrieve single record details.
|
|
166
|
+
- `POST /api/admin/:plural_name` — Create new record.
|
|
167
|
+
- `PUT /api/admin/:plural_name/:id` — Update record (with Optimistic Concurrency Control).
|
|
168
|
+
- `DELETE /api/admin/:plural_name/:id` — Delete record.
|
|
169
|
+
- `GET /api/admin/:plural_name/export` — Export data in `csv` or `xlsx` format (`?format=xlsx`).
|
|
170
|
+
|
|
171
|
+
### Media Uploads
|
|
172
|
+
- `POST /api/admin/uploads` — Upload file via multipart/form-data.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Testing
|
|
177
|
+
|
|
178
|
+
Run the test suite using `rspec`:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
bundle exec rspec
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
data/TUTORIAL.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Roda-Kabk Ultimate Tutorial: Building a Comprehensive Simurgh Admin Panel
|
|
2
|
+
|
|
3
|
+
Welcome to the **Roda-Kabk Tutorial**. This guide will walk you through building a fully-featured, production-ready admin panel using `roda-kabk` and the Simurgh Panel protocol.
|
|
4
|
+
|
|
5
|
+
We will translate a complex, real-world schema—including relations, jalali calendars, advanced validations, JSON editors, custom fonts, and file uploads—directly into the Ruby Kabk DSL.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Setting Up the Application & System Config
|
|
10
|
+
|
|
11
|
+
First, we need to mount the plugin in our Roda application and configure the `system_config`. This is where we define custom branding, multilingual support, and custom fonts (like Sahel and Lalezar).
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'roda'
|
|
15
|
+
require 'roda/plugins/kabk'
|
|
16
|
+
|
|
17
|
+
class AdminApp < Roda
|
|
18
|
+
plugin :kabk,
|
|
19
|
+
mount_at: "/api/admin",
|
|
20
|
+
system_config: {
|
|
21
|
+
title: { fa: "پنل مدیریت سیمرغ", en: "Simurgh Panel" },
|
|
22
|
+
logo_url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=100&h=100&fit=crop&crop=faces",
|
|
23
|
+
default_locale: "fa",
|
|
24
|
+
supported_locales: ["fa", "en"],
|
|
25
|
+
direction: "rtl",
|
|
26
|
+
custom_fonts: [
|
|
27
|
+
{
|
|
28
|
+
name: "Sahel",
|
|
29
|
+
url: "https://cdn.jsdelivr.net/gh/rastikerdar/sahel-font@v3.4.0/dist/Sahel.woff2",
|
|
30
|
+
format: "woff2",
|
|
31
|
+
label: { fa: "ساحل (فارسی)", en: "Sahel (Persian)" }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "Lalezar",
|
|
35
|
+
url: "https://cdn.jsdelivr.net/gh/rastikerdar/lalezar-font@v1.0.0/dist/Lalezar.woff2",
|
|
36
|
+
format": "woff2",
|
|
37
|
+
label: { fa: "لالهزار (عنوان فانتزی)", en: "Lalezar (Display)" }
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
custom_font_sizes: [
|
|
41
|
+
{ size: "15px", label: { fa: "عادی ویژه", en: "Special Normal" } },
|
|
42
|
+
{ size: "20px", label: { fa: "برجسته متوسط", en: "Highlighted Medium" } },
|
|
43
|
+
{ size: "32px", label: { fa: "تیتر بزرگ", en: "Hero Banner" } }
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
auth_strategy: Kabk::Auth::JwtStrategy.new(secret: ENV.fetch("JWT_SECRET", "secret")),
|
|
47
|
+
login_handler: ->(username, password) {
|
|
48
|
+
user = User.first(email: username)
|
|
49
|
+
return nil unless user && user.authenticate(password)
|
|
50
|
+
{ id: user.id, username: user.email, role: user.role }
|
|
51
|
+
},
|
|
52
|
+
change_password_handler: ->(user_context, old_pw, new_pw) {
|
|
53
|
+
user = User[user_context[:id]]
|
|
54
|
+
raise Kabk::InvalidOldPasswordError.new(fields: { old_password: ["Incorrect password"] }) unless user.authenticate(old_pw)
|
|
55
|
+
user.update(password: new_pw)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
route do |r|
|
|
59
|
+
r.kabk
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 2. Defining Resources (The DSL)
|
|
67
|
+
|
|
68
|
+
We will define 5 major resources to showcase different capabilities: `Users`, `Categories`, `Products`, `Support Tickets`, and `News`.
|
|
69
|
+
|
|
70
|
+
### Resource 1: Users (Badges, Textareas, and Unique Validation)
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
Kabk.register(name: "users", table: User) do
|
|
74
|
+
title fa: "مدیریت کاربران", en: "User Directory"
|
|
75
|
+
icon "Users"
|
|
76
|
+
api_path "/api/admin/users"
|
|
77
|
+
group "دسترسی و کاربران"
|
|
78
|
+
order 2
|
|
79
|
+
searchable_fields [:full_name, :email]
|
|
80
|
+
sortable_fields [:id, :full_name, :role, :birth_date, :created_at]
|
|
81
|
+
concurrency_field "updated_at"
|
|
82
|
+
|
|
83
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
84
|
+
|
|
85
|
+
field :full_name, type: :string, form_type: :text, required: true, validation: { min_length: 3, max_length: 100 }
|
|
86
|
+
|
|
87
|
+
field :email, type: :string, form_type: :text, required: true, validation: {
|
|
88
|
+
pattern: "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
|
|
89
|
+
unique: true,
|
|
90
|
+
custom_message: { fa: "فرمت ایمیل معتبر نیست یا قبلاً ثبت شده است", en: "Email invalid or taken" }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
field :avatar, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true
|
|
94
|
+
|
|
95
|
+
field :role, type: :string, form_type: :select, display_as: "badge", required: true, default_value: "editor", options: [
|
|
96
|
+
{ label: { fa: "مدیر کل", en: "Administrator" }, value: "admin", color: "purple" },
|
|
97
|
+
{ label: { fa: "ویرایشگر", en: "Editor" }, value: "editor", color: "blue" },
|
|
98
|
+
{ label: { fa: "نویسنده", en: "Author" }, value: "author", color: "emerald" },
|
|
99
|
+
{ label: { fa: "مشاهدهکننده", en: "Viewer" }, value: "viewer", color: "gray" }
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
field :bio, type: :string, form_type: :textarea, hidden_in_table: true, accordion: true, rows: 3
|
|
103
|
+
field :is_active, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: true
|
|
104
|
+
|
|
105
|
+
field :birth_date, type: :date, form_type: :date, calendar: :jalali, nullable: true
|
|
106
|
+
field :created_at, type: :date, form_type: :date, calendar: :jalali, readonly: true, hidden_in_form: true
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Resource 2: Categories (Simple Text and Badges)
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
Kabk.register(name: "categories", table: Category) do
|
|
114
|
+
title fa: "دستهبندیها", en: "Categories"
|
|
115
|
+
icon "Tags"
|
|
116
|
+
api_path "/api/admin/categories"
|
|
117
|
+
group "مدیریت محتوا"
|
|
118
|
+
order 3
|
|
119
|
+
searchable_fields [:title, :slug]
|
|
120
|
+
|
|
121
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
122
|
+
field :title, type: :string, form_type: :text, required: true, col_width: 6
|
|
123
|
+
field :slug, type: :string, form_type: :text, required: true, col_width: 6, validation: { pattern: "^[a-z0-9-]+$" }
|
|
124
|
+
field :color_code, type: :string, form_type: :text, display_as: "badge", col_width: 6
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Resource 3: Products (JSON Editors and Advanced Layout)
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
Kabk.register(name: "products", table: Product) do
|
|
132
|
+
title fa: "محصولات و کالاها", en: "Products Catalog"
|
|
133
|
+
icon "Package"
|
|
134
|
+
api_path "/api/admin/products"
|
|
135
|
+
group "فروشگاه و خدمات"
|
|
136
|
+
order 4
|
|
137
|
+
searchable_fields [:title, :sku]
|
|
138
|
+
|
|
139
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
140
|
+
field :title, type: :string, form_type: :text, required: true, col_width: 8
|
|
141
|
+
field :sku, type: :string, form_type: :text, required: true, col_width: 4
|
|
142
|
+
|
|
143
|
+
field :price, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0, max: 1000000000 }
|
|
144
|
+
field :stock, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0 }
|
|
145
|
+
|
|
146
|
+
# Using JSON Editor with accordion and default nested structures
|
|
147
|
+
field :specs_json, type: :json, form_type: :json_editor, hidden_in_table: true, accordion: true, default_value: {
|
|
148
|
+
weight_kg: 1.2,
|
|
149
|
+
brand: "Shayan Group",
|
|
150
|
+
warranty_months: 24,
|
|
151
|
+
dimensions: { length: 30, width: 20, height: 10 }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
field :status, type: :string, form_type: :select, display_as: "badge", options: [
|
|
155
|
+
{ label: { fa: "موجود در انبار", en: "In Stock" }, value: "in_stock", color: "green" },
|
|
156
|
+
{ label: { fa: "موجودی محدود", en: "Low Stock" }, value: "low_stock", color: "amber" },
|
|
157
|
+
{ label: { fa: "ناموجود", en: "Out of Stock" }, value: "out_of_stock", color: "red" }
|
|
158
|
+
]
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Resource 4: News (Relations, File Uploads, WYSIWYG, Jalali Date)
|
|
163
|
+
|
|
164
|
+
This entity brings it all together by referencing `users` and `categories`, and handling complex file configurations.
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
Kabk.register(name: "news_item", table: NewsItem) do
|
|
168
|
+
title fa: "اخبار و اطلاعیهها", en: "News & Announcements"
|
|
169
|
+
icon "Newspaper"
|
|
170
|
+
api_path "/api/admin/news"
|
|
171
|
+
group "مدیریت محتوا"
|
|
172
|
+
order 1
|
|
173
|
+
concurrency_field "updated_at"
|
|
174
|
+
|
|
175
|
+
searchable_fields [:title, :content]
|
|
176
|
+
filterable_fields [:published, :category_ids]
|
|
177
|
+
|
|
178
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true, col_width: 2
|
|
179
|
+
|
|
180
|
+
field :title, type: :string, form_type: :text, required: true, col_width: 12, validation: {
|
|
181
|
+
min_length: 5, max_length: 150,
|
|
182
|
+
custom_message: { fa: "عنوان بین ۵ تا ۱۵۰ کاراکتر", en: "Title 5-150 chars" }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Many-to-One Relation (Author -> User)
|
|
186
|
+
field :author_id, type: :relation, form_type: :relation_select, required: true, col_width: 6, relation: {
|
|
187
|
+
resource: "users", cardinality: :many_to_one,
|
|
188
|
+
value_field: "id", label_field: "full_name",
|
|
189
|
+
display_key: "author_id_display", fetch_endpoint: "/api/admin/users",
|
|
190
|
+
search_param: "search", debounce_ms: 300, per_page: 20
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Many-to-Many Relation (News -> Categories)
|
|
194
|
+
field :category_ids, type: :relation, form_type: :multiselect, required: true, col_width: 6, relation: {
|
|
195
|
+
resource: "categories", cardinality: :many_to_many,
|
|
196
|
+
value_field: "id", label_field: "title",
|
|
197
|
+
display_key: "category_ids_display", fetch_endpoint: "/api/admin/categories"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Single Image Upload (with constraints)
|
|
201
|
+
field :cover_image, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true, col_width: 6, upload_config: {
|
|
202
|
+
allowed_extensions: ["jpg", "jpeg", "png", "webp"],
|
|
203
|
+
max_size_mb: 3
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Multi-Image Gallery
|
|
207
|
+
field :gallery, type: :array, form_type: :image_multi, hidden_in_table: true, col_width: 6, upload_config: {
|
|
208
|
+
allowed_extensions: ["jpg", "png", "webp"], max_size_mb: 5
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
field :content, type: :string, form_type: :wysiwyg, hidden_in_table: true, accordion: true, required: true
|
|
212
|
+
|
|
213
|
+
field :published, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: false
|
|
214
|
+
|
|
215
|
+
# Dependent Field with Jalali Calendar
|
|
216
|
+
field :publish_date, type: :datetime, form_type: :datetime, calendar: :jalali, nullable: true, col_width: 6, depends_on: {
|
|
217
|
+
field: "published", value: true
|
|
218
|
+
}, validation: { min_date: "2020-01-01T00:00:00Z" }
|
|
219
|
+
|
|
220
|
+
field :updated_at, type: :datetime, form_type: :datetime, calendar: :jalali, readonly: true, hidden_in_form: true
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 3. What to do next?
|
|
227
|
+
Now that the schema and DSL have been mapped out, simply run your Roda application and navigate to `/api/admin` in your browser. The Simurgh panel will dynamically parse this metadata and render all interactive UI components, Jalali calendars, relationships, validations, and custom fonts.
|
data/examples/app.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "roda"
|
|
4
|
+
require "sequel"
|
|
5
|
+
require "logger"
|
|
6
|
+
|
|
7
|
+
# Setup memory database
|
|
8
|
+
DB = Sequel.sqlite
|
|
9
|
+
DB.extension :pagination
|
|
10
|
+
DB.logger = Logger.new($stdout)
|
|
11
|
+
|
|
12
|
+
# Create tables matching Schema_Sample.json entities
|
|
13
|
+
DB.create_table :users do
|
|
14
|
+
primary_key :id
|
|
15
|
+
String :full_name
|
|
16
|
+
String :email
|
|
17
|
+
String :avatar
|
|
18
|
+
String :role, default: "editor"
|
|
19
|
+
String :bio
|
|
20
|
+
TrueClass :is_active, default: true
|
|
21
|
+
DateTime :created_at
|
|
22
|
+
DateTime :updated_at
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
DB.create_table :categories do
|
|
26
|
+
primary_key :id
|
|
27
|
+
String :title
|
|
28
|
+
String :slug
|
|
29
|
+
String :color_code
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
DB.create_table :news_items do
|
|
33
|
+
primary_key :id
|
|
34
|
+
String :title
|
|
35
|
+
Integer :author_id
|
|
36
|
+
String :category_ids # Simplified many-to-many as JSON string or comma-separated for SQLite
|
|
37
|
+
String :cover_image
|
|
38
|
+
String :gallery
|
|
39
|
+
String :content
|
|
40
|
+
TrueClass :published, default: false
|
|
41
|
+
DateTime :publish_date
|
|
42
|
+
DateTime :updated_at
|
|
43
|
+
DateTime :created_at
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Require Kabk core
|
|
47
|
+
$LOAD_PATH.unshift File.expand_path("../../kabk/lib", __dir__)
|
|
48
|
+
require "kabk"
|
|
49
|
+
|
|
50
|
+
# Define Sequel Models
|
|
51
|
+
class User < Sequel::Model; end
|
|
52
|
+
class Category < Sequel::Model; end
|
|
53
|
+
class NewsItem < Sequel::Model; end
|
|
54
|
+
|
|
55
|
+
# Register Resources using DSL
|
|
56
|
+
Kabk.register(name: "user", table: User) do
|
|
57
|
+
title fa: "مدیریت کاربران", en: "User Directory"
|
|
58
|
+
plural_name "users"
|
|
59
|
+
icon "Users"
|
|
60
|
+
api_path "/api/admin/users"
|
|
61
|
+
searchable_fields ["full_name", "email"]
|
|
62
|
+
sortable_fields ["id", "full_name", "role", "created_at"]
|
|
63
|
+
concurrency_field "updated_at"
|
|
64
|
+
|
|
65
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
66
|
+
field :full_name, type: :string, form_type: :text, required: true, validation: { min_length: 3, max_length: 100 }
|
|
67
|
+
field :email, type: :string, form_type: :text, required: true, validation: { unique: true }
|
|
68
|
+
field :avatar, type: :file, form_type: :image_single, display_as: :thumbnail
|
|
69
|
+
field :role, type: :string, form_type: :select, required: true, display_as: :badge, default_value: "editor"
|
|
70
|
+
field :bio, type: :string, form_type: :textarea, hidden_in_table: true, accordion: true
|
|
71
|
+
field :is_active, type: :boolean, form_type: :switch, display_as: :boolean_icon, default_value: true
|
|
72
|
+
field :created_at, type: :date, form_type: :date, calendar: :jalali, readonly: true, hidden_in_form: true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Kabk.register(name: "category", table: Category) do
|
|
76
|
+
title fa: "دستهبندیها", en: "Categories"
|
|
77
|
+
plural_name "categories"
|
|
78
|
+
icon "Tags"
|
|
79
|
+
searchable_fields ["title", "slug"]
|
|
80
|
+
|
|
81
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
82
|
+
field :title, type: :string, form_type: :text, required: true
|
|
83
|
+
field :slug, type: :string, form_type: :text, required: true
|
|
84
|
+
field :color_code, type: :string, form_type: :text, display_as: :badge
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Kabk.register(name: "news_item", table: NewsItem) do
|
|
88
|
+
title fa: "اخبار و اطلاعیهها", en: "News & Announcements"
|
|
89
|
+
plural_name "news"
|
|
90
|
+
icon "Newspaper"
|
|
91
|
+
searchable_fields ["title", "content"]
|
|
92
|
+
concurrency_field "updated_at"
|
|
93
|
+
|
|
94
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
95
|
+
field :title, type: :string, form_type: :text, required: true
|
|
96
|
+
field :author_id, type: :relation, form_type: :relation_select, relation: { resource: "user", cardinality: "many_to_one", value_field: "id", label_field: "full_name" }
|
|
97
|
+
field :category_ids, type: :relation, form_type: :multiselect, relation: { resource: "category", cardinality: "many_to_many", value_field: "id", label_field: "title" }
|
|
98
|
+
field :cover_image, type: :file, form_type: :image_single, display_as: :thumbnail
|
|
99
|
+
field :content, type: :string, form_type: :wysiwyg, hidden_in_table: true, accordion: true
|
|
100
|
+
field :published, type: :boolean, form_type: :switch, display_as: :boolean_icon
|
|
101
|
+
field :publish_date, type: :datetime, form_type: :datetime, calendar: :jalali, depends_on: { field: "published", value: true }
|
|
102
|
+
field :updated_at, type: :datetime, form_type: :datetime, calendar: :jalali, readonly: true, hidden_in_form: true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Seed some mock data
|
|
106
|
+
User.create(full_name: "Admin User", email: "admin@example.com", role: "admin")
|
|
107
|
+
User.create(full_name: "Reza Mohammadi", email: "reza@example.com", role: "editor")
|
|
108
|
+
Category.create(title: "Announcements", slug: "announcements")
|
|
109
|
+
Category.create(title: "Events", slug: "events")
|
|
110
|
+
NewsItem.create(title: "First Post", author_id: 2, category_ids: "[1,2]", content: "Hello World", updated_at: Time.now)
|
|
111
|
+
|
|
112
|
+
# Main App using Roda
|
|
113
|
+
class App < Roda
|
|
114
|
+
# Setup plugin with mock JWT Strategy
|
|
115
|
+
plugin :kabk,
|
|
116
|
+
mount_at: "/api/admin",
|
|
117
|
+
auth_strategy: Kabk::Auth::JwtStrategy.new(secret: "test_secret"),
|
|
118
|
+
system_config: { logo_url: "/kabk_logo.png" }
|
|
119
|
+
|
|
120
|
+
route do |r|
|
|
121
|
+
# This mounts all the standard protocol endpoints and UI automatically under /api/admin
|
|
122
|
+
r.kabk
|
|
123
|
+
|
|
124
|
+
r.root do
|
|
125
|
+
"Welcome to Kabk Simurgh Protocol API."
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/examples/config.ru
ADDED