roda-kabk 1.0.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/Gemfile +10 -0
- data/Gemfile.lock +79 -0
- data/LICENSE +21 -0
- data/README.md +267 -0
- data/TUTORIAL.md +271 -0
- data/examples/app.rb +218 -0
- data/examples/config.ru +2 -0
- data/lib/roda/plugins/kabk/helpers.rb +326 -0
- data/lib/roda/plugins/kabk.rb +53 -0
- data/public/assets/scripts.js +8638 -0
- data/public/assets/style.css +1 -0
- data/public/index.html +126 -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: f5508b1dac33bf7629b9c147a7f1798a67e78280862aaacc77fa6114944df277
|
|
4
|
+
data.tar.gz: 459908e88eb476e96592aa97f8832c53c5bbcd01b446ace3e977b6aca14c1bea
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e3be59177e76c02daad67d4d00c1c6b6c6effd42ad0acee508e1b75b85516760552f0e1839c47e9d9eccc4bec16c6f2f171619f14c533b5dfc4afa001f1302dc
|
|
7
|
+
data.tar.gz: 1f322e901a43c63bb5ceaec3ebb6314bed76c3384a951e144b79ddf68c7b6b13c189768f03f57d516da856d3bc9680f7a56923dc9e62b248264b6bc617d7d587
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
roda-kabk (0.2.0)
|
|
5
|
+
kabk (~> 0.2.0)
|
|
6
|
+
roda (~> 3.0)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
bigdecimal (4.1.2)
|
|
12
|
+
concurrent-ruby (1.3.8)
|
|
13
|
+
diff-lcs (1.6.2)
|
|
14
|
+
kabk (0.2.0)
|
|
15
|
+
concurrent-ruby (~> 1.1)
|
|
16
|
+
sequel (~> 5.0)
|
|
17
|
+
logger (1.7.0)
|
|
18
|
+
nio4r (2.7.5)
|
|
19
|
+
puma (8.0.2)
|
|
20
|
+
nio4r (~> 2.0)
|
|
21
|
+
rack (3.2.6)
|
|
22
|
+
rack-test (2.2.0)
|
|
23
|
+
rack (>= 1.3)
|
|
24
|
+
rackup (2.3.1)
|
|
25
|
+
rack (>= 3)
|
|
26
|
+
roda (3.106.0)
|
|
27
|
+
rack
|
|
28
|
+
rspec (3.13.2)
|
|
29
|
+
rspec-core (~> 3.13.0)
|
|
30
|
+
rspec-expectations (~> 3.13.0)
|
|
31
|
+
rspec-mocks (~> 3.13.0)
|
|
32
|
+
rspec-core (3.13.6)
|
|
33
|
+
rspec-support (~> 3.13.0)
|
|
34
|
+
rspec-expectations (3.13.5)
|
|
35
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
36
|
+
rspec-support (~> 3.13.0)
|
|
37
|
+
rspec-mocks (3.13.8)
|
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
39
|
+
rspec-support (~> 3.13.0)
|
|
40
|
+
rspec-support (3.13.7)
|
|
41
|
+
sequel (5.107.0)
|
|
42
|
+
bigdecimal
|
|
43
|
+
sqlite3 (2.9.5-x64-mingw-ucrt)
|
|
44
|
+
|
|
45
|
+
PLATFORMS
|
|
46
|
+
x64-mingw-ucrt
|
|
47
|
+
|
|
48
|
+
DEPENDENCIES
|
|
49
|
+
logger
|
|
50
|
+
puma
|
|
51
|
+
rack-test
|
|
52
|
+
rackup
|
|
53
|
+
roda-kabk!
|
|
54
|
+
rspec
|
|
55
|
+
sqlite3
|
|
56
|
+
|
|
57
|
+
CHECKSUMS
|
|
58
|
+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
59
|
+
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
|
|
60
|
+
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
61
|
+
kabk (0.2.0) sha256=96b8cbea6b28d8ef0e5b9b4d935851833a617d87f14c7b0c39bc50142bf78118
|
|
62
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
63
|
+
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
|
64
|
+
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
|
|
65
|
+
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
|
|
66
|
+
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
|
67
|
+
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
|
68
|
+
roda (3.106.0) sha256=1a1cfb13c6690db158a3929e9c9ece88e697fb23b05baae63f07b0773e3bbade
|
|
69
|
+
roda-kabk (0.2.0)
|
|
70
|
+
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
71
|
+
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
72
|
+
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
73
|
+
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
|
|
74
|
+
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
|
|
75
|
+
sequel (5.107.0) sha256=019629313205295b007f6df2c5eab19ddc40710d549fd445ac300f2853a370db
|
|
76
|
+
sqlite3 (2.9.5-x64-mingw-ucrt) sha256=b00d5697994ee8589b6096694a2130aa5567db64373baca55ea98c9bf958f46a
|
|
77
|
+
|
|
78
|
+
BUNDLED WITH
|
|
79
|
+
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,267 @@
|
|
|
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) — providing a **Smart Dispatch** routing architecture for the Simurgh Panel protocol (v1.6.0). It uses `api_path` defined in `Kabk.register` as the Single Source of Truth for all routes, giving you full control over middleware, authentication, and routing branches.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Smart Dispatch Routing (`r.kabk.route`)**: Automatically routes incoming requests to the appropriate `Kabk::RestEngine` resource based on each resource's `api_path`.
|
|
21
|
+
- **Global Static Assets (`r.kabk.statics`)**: Serves pre-built assets (JS, CSS, SVGs, images, fonts, videos) under `/assets/*` and `/simurgh-logo.svg`.
|
|
22
|
+
- **Vue.js SPA Server (`r.kabk.server`)**: Serves the single-page admin dashboard SPA `index.html` with embedded schema injection.
|
|
23
|
+
- **Protocol Schema Manifest (`r.kabk.schema`)**: Exposes dynamic protocol schema JSON.
|
|
24
|
+
- **Pluggable File Uploads (`r.kabk.upload`)**: Seamless integration with **Shrine**, ActiveStorage, or custom storage handlers via configurable procs.
|
|
25
|
+
- **Context Injection**: Pass `context` into `r.kabk.route(context: ...)` to support Kabk's lifecycle hooks and audit fields based on the current user.
|
|
26
|
+
- **Optimistic Concurrency Control (OCC)**: Built-in support for preventing concurrent record overwrite conflicts (`updated_at` / `version`).
|
|
27
|
+
- **Client-Side Data Export**: CSV exports are natively generated on the client-side (frontend) using existing table data and active filters without backend overhead.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Add this line to your application's `Gemfile`:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem 'roda-kabk'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
And then execute:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bundle install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick Start (Smart Dispatch Architecture)
|
|
48
|
+
|
|
49
|
+
In the new decoupled architecture, `Kabk` no longer manages authentication internally. You handle authentication in Roda and dispatch protected CRUD requests via `r.kabk.route(context: ...)`.
|
|
50
|
+
|
|
51
|
+
### 1. Setup DB and Sequel Model
|
|
52
|
+
|
|
53
|
+
Define your user and resource models with `api_path` as the Single Source of Truth:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
require 'roda'
|
|
57
|
+
require 'sequel'
|
|
58
|
+
require 'kabk'
|
|
59
|
+
require 'roda/plugins/kabk'
|
|
60
|
+
require 'bcrypt'
|
|
61
|
+
|
|
62
|
+
DB = Sequel.sqlite
|
|
63
|
+
|
|
64
|
+
# Authentication Model
|
|
65
|
+
DB.create_table :admin_users do
|
|
66
|
+
primary_key :id
|
|
67
|
+
String :username, null: false, unique: true
|
|
68
|
+
String :password_digest, null: false
|
|
69
|
+
String :full_name
|
|
70
|
+
String :avatar_url
|
|
71
|
+
String :role, default: "admin"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class AdminUser < Sequel::Model
|
|
75
|
+
plugin :secure_password
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
DB.create_table :articles do
|
|
79
|
+
primary_key :id
|
|
80
|
+
String :title, null: false
|
|
81
|
+
String :content
|
|
82
|
+
DateTime :updated_at
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class Article < Sequel::Model; end
|
|
86
|
+
|
|
87
|
+
# Register Entity with Kabk (api_path is the Single Source of Truth)
|
|
88
|
+
Kabk.register(name: "article", table: Article) do
|
|
89
|
+
title "Articles"
|
|
90
|
+
plural_name "articles"
|
|
91
|
+
api_path "/admin/api/articles"
|
|
92
|
+
concurrency_field "updated_at"
|
|
93
|
+
|
|
94
|
+
field :id, type: :number, form_type: :number, primary_key: true
|
|
95
|
+
field :title, type: :string, form_type: :text, required: true
|
|
96
|
+
field :content, type: :string, form_type: :textarea
|
|
97
|
+
field :updated_at, type: :datetime, form_type: :datetime, readonly: true
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 2. Implement Roda Application
|
|
102
|
+
|
|
103
|
+
Use Roda's routing tree with `r.kabk` helper methods:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class App < Roda
|
|
107
|
+
plugin :sessions, secret: ENV.fetch("SESSION_SECRET", "super_secret_key_that_is_long_enough_for_session")
|
|
108
|
+
plugin :json
|
|
109
|
+
plugin :kabk,
|
|
110
|
+
system_config: {
|
|
111
|
+
title: { en: "Admin Dashboard", fa: "پنل مدیریت" },
|
|
112
|
+
default_locale: "en",
|
|
113
|
+
supported_locales: ["en", "fa"],
|
|
114
|
+
direction: "ltr",
|
|
115
|
+
show_demo_credentials: false,
|
|
116
|
+
endpoints: {
|
|
117
|
+
upload: "/admin/api/uploads"
|
|
118
|
+
},
|
|
119
|
+
auth: {
|
|
120
|
+
strategy: "session",
|
|
121
|
+
sso_redirect_url: nil,
|
|
122
|
+
login_url: "/admin/api/auth/login",
|
|
123
|
+
me_url: "/admin/api/auth/me",
|
|
124
|
+
logout_url: "/admin/api/auth/logout",
|
|
125
|
+
refresh_url: "/admin/api/auth/refresh",
|
|
126
|
+
show_demo_credentials: false,
|
|
127
|
+
login_fields: [
|
|
128
|
+
{ name: "username", label: { en: "Username", fa: "نام کاربری" }, type: "text", required: true },
|
|
129
|
+
{ name: "password", label: { en: "Password", fa: "کلمه عبور" }, type: "password", required: true }
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
upload_handler: ->(file_param, req) {
|
|
134
|
+
{
|
|
135
|
+
url: "/uploads/#{file_param[:filename]}",
|
|
136
|
+
file_name: file_param[:filename],
|
|
137
|
+
size: file_param[:tempfile].size,
|
|
138
|
+
mime_type: file_param[:type]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
route do |r|
|
|
143
|
+
r.kabk.statics
|
|
144
|
+
|
|
145
|
+
r.on "admin" do
|
|
146
|
+
r.is do
|
|
147
|
+
r.kabk.server
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
r.on "api" do
|
|
151
|
+
r.get "schema" do
|
|
152
|
+
r.kabk.schema
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
r.post "uploads" do
|
|
156
|
+
r.kabk.upload
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
r.on "auth" do
|
|
160
|
+
r.post "login" do
|
|
161
|
+
body = JSON.parse(r.body.read) rescue {}
|
|
162
|
+
user = AdminUser.first(username: body["username"])
|
|
163
|
+
if user && user.authenticate(body["password"])
|
|
164
|
+
session[:user_id] = user.id
|
|
165
|
+
{ success: true, data: { id: user.id, username: user.username, role: user.role } }
|
|
166
|
+
else
|
|
167
|
+
response.status = 401
|
|
168
|
+
{ success: false, error: { message: "Invalid credentials" } }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
r.get "me" do
|
|
173
|
+
user = AdminUser[session[:user_id]]
|
|
174
|
+
if user
|
|
175
|
+
{ success: true, data: { id: user.id, username: user.username, role: user.role } }
|
|
176
|
+
else
|
|
177
|
+
response.status = 401
|
|
178
|
+
{ success: false, error: { message: "Unauthorized" } }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
r.post "logout" do
|
|
183
|
+
session.clear
|
|
184
|
+
{ success: true, message: "Logged out" }
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
current_user = AdminUser[session[:user_id]]
|
|
189
|
+
unless current_user
|
|
190
|
+
response.status = 401
|
|
191
|
+
r.halt({ success: false, error: { message: "Unauthorized" } }.to_json)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
r.kabk.route(context: { current_user: current_user })
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Configuration Reference
|
|
204
|
+
|
|
205
|
+
The `plugin :kabk` method accepts the following options:
|
|
206
|
+
|
|
207
|
+
| Option | Type | Default | Description |
|
|
208
|
+
| :--- | :--- | :--- | :--- |
|
|
209
|
+
| `system_config` | `Hash` / `NilClass` | `nil` | Custom system branding, custom fonts, and localization settings. |
|
|
210
|
+
| `upload_handler` | `Proc` / `Callable` | `nil` | Custom file upload handler block/callable. Processes uploaded file params. |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Request Methods (`r.kabk.*`)
|
|
215
|
+
|
|
216
|
+
| Method | Description |
|
|
217
|
+
| :--- | :--- |
|
|
218
|
+
| `r.kabk.statics` | Serves static assets (JS, CSS, SVGs, images, fonts, videos) from `/assets/*` and `simurgh-logo.svg`. |
|
|
219
|
+
| `r.kabk.server` | Serves `index.html` (with memoized schema injection) for the Vue.js SPA dashboard. |
|
|
220
|
+
| `r.kabk.schema` | Returns the dynamic protocol schema JSON manifest. |
|
|
221
|
+
| `r.kabk.upload` | Handles multipart file uploads using `upload_handler` or fallback. |
|
|
222
|
+
| `r.kabk.route(context: {})` | Smart Dispatcher: matches the request path against registered `api_path`s and executes CRUD actions (list, get, create, update, delete) on `Kabk::RestEngine`. |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Context Injection & Hooks
|
|
227
|
+
|
|
228
|
+
By passing `context: { current_user: ... }` to `r.kabk.route`, `Kabk`'s `RestEngine` propagates this context to lifecycle hooks and audit fields. For example, you can write entity hooks in `Kabk` core that verify if a user has permission to delete a record or automatically assign the `created_by` field based on `context[:current_user]`.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## File Upload Integration (Shrine Example)
|
|
233
|
+
|
|
234
|
+
You can connect **Shrine** directly using the `upload_handler` option:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
plugin :kabk,
|
|
238
|
+
upload_handler: ->(file_param, req) {
|
|
239
|
+
# file_param contains Rack tempfile params: { tempfile: #<File>, filename: "...", type: "..." }
|
|
240
|
+
uploader = ImageUploader.upload(file_param[:tempfile], :store)
|
|
241
|
+
|
|
242
|
+
{
|
|
243
|
+
url: uploader.url,
|
|
244
|
+
file_name: file_param[:filename],
|
|
245
|
+
size: file_param[:tempfile].size,
|
|
246
|
+
mime_type: file_param[:type]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
If no `upload_handler` is provided and no file is attached, `r.kabk.upload` returns an `HTTP 422 Unprocessable Entity` error.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Testing
|
|
256
|
+
|
|
257
|
+
Run the test suite using `rspec`:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
bundle exec rspec
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
data/TUTORIAL.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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, 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.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'roda'
|
|
15
|
+
require 'sequel'
|
|
16
|
+
require 'kabk'
|
|
17
|
+
require 'roda/plugins/kabk'
|
|
18
|
+
|
|
19
|
+
class AdminApp < Roda
|
|
20
|
+
plugin :sessions, secret: ENV.fetch("SESSION_SECRET", "super_secret_key_that_is_long_enough_for_session")
|
|
21
|
+
plugin :json
|
|
22
|
+
plugin :kabk,
|
|
23
|
+
system_config: {
|
|
24
|
+
title: { en: "Simurgh Panel", fa: "پنل مدیریت سیمرغ" },
|
|
25
|
+
logo_url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=100&h=100&fit=crop&crop=faces",
|
|
26
|
+
default_locale: "en",
|
|
27
|
+
supported_locales: ["en", "fa"],
|
|
28
|
+
direction: "ltr",
|
|
29
|
+
show_demo_credentials: false,
|
|
30
|
+
custom_fonts: [
|
|
31
|
+
{
|
|
32
|
+
name: "Inter",
|
|
33
|
+
url: "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2",
|
|
34
|
+
format: "woff2",
|
|
35
|
+
label: { en: "Inter (English)", fa: "اینتر (انگلیسی)" }
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "Sahel",
|
|
39
|
+
url: "https://cdn.jsdelivr.net/gh/rastikerdar/sahel-font@v3.4.0/dist/Sahel.woff2",
|
|
40
|
+
format: "woff2",
|
|
41
|
+
label: { en: "Sahel (Persian)", fa: "ساحل (فارسی)" }
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
custom_font_sizes: [
|
|
45
|
+
{ size: "14px", label: { en: "Standard", fa: "عادی" } },
|
|
46
|
+
{ size: "18px", label: { en: "Medium", fa: "متوسط" } },
|
|
47
|
+
{ size: "28px", label: { en: "Large Heading", fa: "تیتر بزرگ" } }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
route do |r|
|
|
52
|
+
r.kabk.statics
|
|
53
|
+
|
|
54
|
+
r.on "admin" do
|
|
55
|
+
r.is do
|
|
56
|
+
r.kabk.server
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
r.on "api" do
|
|
60
|
+
r.get "schema" do
|
|
61
|
+
r.kabk.schema
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
r.post "uploads" do
|
|
65
|
+
r.kabk.upload
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
r.on "auth" do
|
|
69
|
+
r.post "login" do
|
|
70
|
+
body = JSON.parse(r.body.read) rescue {}
|
|
71
|
+
user = User.first(email: body["username"] || body["email"])
|
|
72
|
+
if user && user.authenticate(body["password"])
|
|
73
|
+
session[:user_id] = user.id
|
|
74
|
+
{ success: true, data: { id: user.id, username: user.email, role: user.role } }
|
|
75
|
+
else
|
|
76
|
+
response.status = 401
|
|
77
|
+
{ success: false, error: { message: "Invalid credentials" } }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
r.get "me" do
|
|
82
|
+
user = User[session[:user_id]]
|
|
83
|
+
if user
|
|
84
|
+
{ success: true, data: { id: user.id, username: user.email, role: user.role } }
|
|
85
|
+
else
|
|
86
|
+
response.status = 401
|
|
87
|
+
{ success: false, error: { message: "Unauthorized" } }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
r.post "logout" do
|
|
92
|
+
session.clear
|
|
93
|
+
{ success: true, message: "Logged out" }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
current_user = User[session[:user_id]]
|
|
98
|
+
unless current_user
|
|
99
|
+
response.status = 401
|
|
100
|
+
r.halt({ error: "Unauthorized" }.to_json)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
r.kabk.route(context: { current_user: current_user })
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 2. Defining Resources (The DSL)
|
|
113
|
+
|
|
114
|
+
We will define 5 major resources to showcase different capabilities: `Users`, `Categories`, `Products`, `Support Tickets`, and `News`.
|
|
115
|
+
|
|
116
|
+
### Resource 1: Users (Badges, Textareas, and Unique Validation)
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
Kabk.register(name: "users", table: User) do
|
|
120
|
+
title en: "User Directory", fa: "مدیریت کاربران"
|
|
121
|
+
icon "Users"
|
|
122
|
+
api_path "/admin/api/users"
|
|
123
|
+
group "Access & Users"
|
|
124
|
+
order 2
|
|
125
|
+
searchable_fields [:full_name, :email]
|
|
126
|
+
sortable_fields [:id, :full_name, :role, :birth_date, :created_at]
|
|
127
|
+
concurrency_field "updated_at"
|
|
128
|
+
|
|
129
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
130
|
+
|
|
131
|
+
field :full_name, type: :string, form_type: :text, required: true, validation: { min_length: 3, max_length: 100 }
|
|
132
|
+
|
|
133
|
+
field :email, type: :string, form_type: :text, required: true, validation: {
|
|
134
|
+
pattern: "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
|
|
135
|
+
unique: true,
|
|
136
|
+
custom_message: { en: "Email invalid or taken", fa: "فرمت ایمیل معتبر نیست یا قبلاً ثبت شده است" }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
field :avatar, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true
|
|
140
|
+
|
|
141
|
+
field :role, type: :string, form_type: :select, display_as: "badge", required: true, default_value: "editor", options: [
|
|
142
|
+
{ label: { en: "Administrator", fa: "مدیر کل" }, value: "admin", color: "purple" },
|
|
143
|
+
{ label: { en: "Editor", fa: "ویرایشگر" }, value: "editor", color: "blue" },
|
|
144
|
+
{ label: { en: "Author", fa: "نویسنده" }, value: "author", color: "emerald" },
|
|
145
|
+
{ label: { en: "Viewer", fa: "مشاهدهکننده" }, value: "viewer", color: "gray" }
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
field :bio, type: :string, form_type: :textarea, hidden_in_table: true, accordion: true, rows: 3
|
|
149
|
+
field :is_active, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: true
|
|
150
|
+
|
|
151
|
+
field :birth_date, type: :date, form_type: :date, nullable: true
|
|
152
|
+
field :created_at, type: :date, form_type: :date, readonly: true, hidden_in_form: true
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Resource 2: Categories (Simple Text and Badges)
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
Kabk.register(name: "categories", table: Category) do
|
|
160
|
+
title en: "Categories", fa: "دستهبندیها"
|
|
161
|
+
icon "Tags"
|
|
162
|
+
api_path "/admin/api/categories"
|
|
163
|
+
group "Content Management"
|
|
164
|
+
order 3
|
|
165
|
+
searchable_fields [:title, :slug]
|
|
166
|
+
|
|
167
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
168
|
+
field :title, type: :string, form_type: :text, required: true, col_width: 6
|
|
169
|
+
field :slug, type: :string, form_type: :text, required: true, col_width: 6, validation: { pattern: "^[a-z0-9-]+$" }
|
|
170
|
+
field :color_code, type: :string, form_type: :text, display_as: "badge", col_width: 6
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Resource 3: Products (JSON Editors and Advanced Layout)
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
Kabk.register(name: "products", table: Product) do
|
|
178
|
+
title en: "Products Catalog", fa: "محصولات و کالاها"
|
|
179
|
+
icon "Package"
|
|
180
|
+
api_path "/admin/api/products"
|
|
181
|
+
group "Store & Services"
|
|
182
|
+
order 4
|
|
183
|
+
searchable_fields [:title, :sku]
|
|
184
|
+
|
|
185
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
|
|
186
|
+
field :title, type: :string, form_type: :text, required: true, col_width: 8
|
|
187
|
+
field :sku, type: :string, form_type: :text, required: true, col_width: 4
|
|
188
|
+
|
|
189
|
+
field :price, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0, max: 1000000000 }
|
|
190
|
+
field :stock, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0 }
|
|
191
|
+
|
|
192
|
+
field :specs_json, type: :json, form_type: :json_editor, hidden_in_table: true, accordion: true, default_value: {
|
|
193
|
+
weight_kg: 1.2,
|
|
194
|
+
brand: "Acme Corp",
|
|
195
|
+
warranty_months: 24,
|
|
196
|
+
dimensions: { length: 30, width: 20, height: 10 }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
field :status, type: :string, form_type: :select, display_as: "badge", options: [
|
|
200
|
+
{ label: { en: "In Stock", fa: "موجود در انبار" }, value: "in_stock", color: "green" },
|
|
201
|
+
{ label: { en: "Low Stock", fa: "موجودی محدود" }, value: "low_stock", color: "amber" },
|
|
202
|
+
{ label: { en: "Out of Stock", fa: "ناموجود" }, value: "out_of_stock", color: "red" }
|
|
203
|
+
]
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Resource 4: News (Relations, File Uploads, WYSIWYG)
|
|
208
|
+
|
|
209
|
+
This entity brings it all together by referencing `users` and `categories`, and handling complex file configurations.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
Kabk.register(name: "news_item", table: NewsItem) do
|
|
213
|
+
title en: "News & Announcements", fa: "اخبار و اطلاعیهها"
|
|
214
|
+
icon "Newspaper"
|
|
215
|
+
api_path "/admin/api/news"
|
|
216
|
+
group "Content Management"
|
|
217
|
+
order 1
|
|
218
|
+
concurrency_field "updated_at"
|
|
219
|
+
|
|
220
|
+
searchable_fields [:title, :content]
|
|
221
|
+
filterable_fields [:published, :category_ids]
|
|
222
|
+
|
|
223
|
+
field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true, col_width: 2
|
|
224
|
+
|
|
225
|
+
field :title, type: :string, form_type: :text, required: true, col_width: 12, validation: {
|
|
226
|
+
min_length: 5, max_length: 150,
|
|
227
|
+
custom_message: { en: "Title 5-150 chars", fa: "عنوان بین ۵ تا ۱۵۰ کاراکتر" }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Many-to-One Relation (Author -> User)
|
|
231
|
+
field :author_id, type: :relation, form_type: :relation_select, required: true, col_width: 6, relation: {
|
|
232
|
+
resource: "users", cardinality: :many_to_one,
|
|
233
|
+
value_field: "id", label_field: "full_name",
|
|
234
|
+
display_key: "author_id_display", fetch_endpoint: "/admin/api/users",
|
|
235
|
+
search_param: "search", debounce_ms: 300, per_page: 20
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Many-to-Many Relation (News -> Categories)
|
|
239
|
+
field :category_ids, type: :relation, form_type: :multiselect, required: true, col_width: 6, relation: {
|
|
240
|
+
resource: "categories", cardinality: :many_to_many,
|
|
241
|
+
value_field: "id", label_field: "title",
|
|
242
|
+
display_key: "category_ids_display", fetch_endpoint: "/admin/api/categories"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Single Image Upload (with constraints)
|
|
246
|
+
field :cover_image, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true, col_width: 6, upload_config: {
|
|
247
|
+
allowed_extensions: ["jpg", "jpeg", "png", "webp"],
|
|
248
|
+
max_size_mb: 3
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Multi-Image Gallery
|
|
252
|
+
field :gallery, type: :array, form_type: :image_multi, hidden_in_table: true, col_width: 6, upload_config: {
|
|
253
|
+
allowed_extensions: ["jpg", "png", "webp"], max_size_mb: 5
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
field :content, type: :string, form_type: :wysiwyg, hidden_in_table: true, accordion: true, required: true
|
|
257
|
+
|
|
258
|
+
field :published, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: false
|
|
259
|
+
|
|
260
|
+
field :publish_date, type: :datetime, form_type: :datetime, nullable: true, col_width: 6, depends_on: {
|
|
261
|
+
field: "published", value: true
|
|
262
|
+
}, validation: { min_date: "2020-01-01T00:00:00Z" }
|
|
263
|
+
|
|
264
|
+
field :updated_at, type: :datetime, form_type: :datetime, readonly: true, hidden_in_form: true
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 3. What to do next?
|
|
271
|
+
Now that the schema and DSL have been mapped out, simply run your Roda application and navigate to `/admin` in your browser. The Simurgh panel will dynamically parse this metadata and render all interactive UI components, relationships, validations, and custom fonts.
|