Roda Kabk Logo
Roda Kabk

Gem Version License: MIT

Kabk carries the Ruby to Simorgh

roda-kabk is the official Roda plugin for 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.


Features

  • Smart Dispatch Routing (r.kabk.route): Automatically routes incoming requests to the appropriate Kabk::RestEngine resource based on each resource's api_path.
  • Global Static Assets (r.kabk.statics): Serves pre-built assets (JS, CSS, SVGs, images, fonts, videos) under /assets/* and /simurgh-logo.svg.
  • Vue.js SPA Server (r.kabk.server): Serves the single-page admin dashboard SPA index.html with embedded schema injection.
  • Protocol Schema Manifest (r.kabk.schema): Exposes dynamic protocol schema JSON.
  • Pluggable File Uploads (r.kabk.upload): Seamless integration with Shrine, ActiveStorage, or custom storage handlers via configurable procs.
  • Context Injection: Pass context into r.kabk.route(context: ...) to support Kabk's lifecycle hooks and audit fields based on the current user.
  • Optimistic Concurrency Control (OCC): Built-in support for preventing concurrent record overwrite conflicts (updated_at / version).
  • Client-Side Data Export: CSV exports are natively generated on the client-side (frontend) using existing table data and active filters without backend overhead.

Installation

Add this line to your application's Gemfile:

gem 'roda-kabk'

And then execute:

bundle install

Quick Start (Smart Dispatch Architecture)

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: ...).

1. Setup DB and Sequel Model

Define your user and resource models with api_path as the Single Source of Truth:

require 'roda'
require 'sequel'
require 'kabk'
require 'roda/plugins/kabk'
require 'bcrypt'

DB = Sequel.sqlite

# Authentication Model
DB.create_table :admin_users do
  primary_key :id
  String :username, null: false, unique: true
  String :password_digest, null: false
  String :full_name
  String :avatar_url
  String :role, default: "admin"
end

class AdminUser < Sequel::Model
  plugin :secure_password
end

DB.create_table :articles do
  primary_key :id
  String :title, null: false
  String :content
  DateTime :updated_at
end

class Article < Sequel::Model; end

# Register Entity with Kabk (api_path is the Single Source of Truth)
Kabk.register(name: "article", table: Article) do
  title "Articles"
  plural_name "articles"
  api_path "/admin/api/articles"
  concurrency_field "updated_at"

  field :id, type: :number, form_type: :number, primary_key: true
  field :title, type: :string, form_type: :text, required: true
  field :content, type: :string, form_type: :textarea
  field :updated_at, type: :datetime, form_type: :datetime, readonly: true
end

2. Implement Roda Application

Use Roda's routing tree with r.kabk helper methods:

class App < Roda
  plugin :sessions, secret: ENV.fetch("SESSION_SECRET", "super_secret_key_that_is_long_enough_for_session")
  plugin :json
  plugin :kabk,
         system_config: {
           title: { en: "Admin Dashboard", fa: "پنل مدیریت" },
           default_locale: "en",
           supported_locales: ["en", "fa"],
           direction: "ltr",
           endpoints: {
             upload: "/admin/api/uploads"
           },
           auth: {
             strategy: "session",
             sso_redirect_url: nil,
             login_url: "/admin/api/auth/login",
             me_url: "/admin/api/auth/me",
             logout_url: "/admin/api/auth/logout",
             refresh_url: "/admin/api/auth/refresh",
             show_demo_credentials: false,
             login_fields: [
               { name: "username", label: { en: "Username", fa: "نام کاربری" }, type: "text", required: true },
               { name: "password", label: { en: "Password", fa: "کلمه عبور" }, type: "password", required: true }
             ]
           }
         },
         upload_handler: ->(file_param, req) {
           {
             url: "/uploads/#{file_param[:filename]}",
             file_name: file_param[:filename],
             size: file_param[:tempfile].size,
             mime_type: file_param[:type]
           }
         }

  route do |r|
    r.kabk.statics

    r.on "admin" do
      r.is do
        r.kabk.server
      end

      r.on "api" do
        r.get "schema" do
          r.kabk.schema
        end

        r.post "uploads" do
          r.kabk.upload
        end

        r.on "auth" do
          r.post "login" do
            body = JSON.parse(r.body.read) rescue {}
            user = AdminUser.first(username: body["username"])
            if user && user.authenticate(body["password"])
              session[:user_id] = user.id
              { success: true, data: { id: user.id, username: user.username, role: user.role } }
            else
              response.status = 401
              { success: false, error: { message: "Invalid credentials" } }
            end
          end

          r.get "me" do
            user = AdminUser[session[:user_id]]
            if user
              { success: true, data: { id: user.id, username: user.username, role: user.role } }
            else
              response.status = 401
              { success: false, error: { message: "Unauthorized" } }
            end
          end

          r.post "logout" do
            session.clear
            { success: true, message: "Logged out" }
          end
        end

        current_user = AdminUser[session[:user_id]]
        unless current_user
          response.status = 401
          r.halt({ success: false, error: { message: "Unauthorized" } }.to_json)
        end

        r.kabk.route(context: { current_user: current_user })
      end
    end
  end
end

Configuration Reference

The plugin :kabk method accepts the following options:

Option Type Default Description
system_config Hash / NilClass nil Custom system branding, custom fonts, and localization settings.
upload_handler Proc / Callable nil Custom file upload handler block/callable. Processes uploaded file params.

Request Methods (r.kabk.*)

Method Description
r.kabk.statics Serves static assets (JS, CSS, SVGs, images, fonts, videos) from /assets/* and simurgh-logo.svg.
r.kabk.server Serves index.html (with memoized schema injection) for the Vue.js SPA dashboard.
r.kabk.schema Returns the dynamic protocol schema JSON manifest.
r.kabk.upload Handles multipart file uploads using upload_handler or fallback.
r.kabk.route(context: {}) Smart Dispatcher: matches the request path against registered api_paths and executes CRUD actions (list, get, create, update, delete) on Kabk::RestEngine.

Context Injection & Hooks

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].


File Upload Integration (Shrine Example)

You can connect Shrine directly using the upload_handler option:

plugin :kabk,
       upload_handler: ->(file_param, req) {
         # file_param contains Rack tempfile params: { tempfile: #<File>, filename: "...", type: "..." }
         uploader = ImageUploader.upload(file_param[:tempfile], :store)

         {
           url: uploader.url,
           file_name: file_param[:filename],
           size: file_param[:tempfile].size,
           mime_type: file_param[:type]
         }
       }

If no upload_handler is provided and no file is attached, r.kabk.upload returns an HTTP 422 Unprocessable Entity error.


Testing

Run the test suite using rspec:

bundle exec rspec

License

The gem is available as open source under the terms of the MIT License.