tina4ruby 3.0.0 → 3.9.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +120 -32
  3. data/lib/tina4/auth.rb +137 -27
  4. data/lib/tina4/auto_crud.rb +55 -3
  5. data/lib/tina4/cli.rb +228 -28
  6. data/lib/tina4/cors.rb +1 -1
  7. data/lib/tina4/database.rb +230 -26
  8. data/lib/tina4/database_result.rb +122 -8
  9. data/lib/tina4/dev_mailbox.rb +1 -1
  10. data/lib/tina4/env.rb +1 -1
  11. data/lib/tina4/frond.rb +314 -7
  12. data/lib/tina4/gallery/queue/meta.json +1 -1
  13. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +314 -16
  14. data/lib/tina4/localization.rb +1 -1
  15. data/lib/tina4/messenger.rb +111 -33
  16. data/lib/tina4/middleware.rb +349 -1
  17. data/lib/tina4/migration.rb +132 -11
  18. data/lib/tina4/orm.rb +149 -18
  19. data/lib/tina4/public/js/tina4-dev-admin.min.js +1 -1
  20. data/lib/tina4/public/js/tina4js.min.js +47 -0
  21. data/lib/tina4/query_builder.rb +374 -0
  22. data/lib/tina4/queue.rb +219 -61
  23. data/lib/tina4/queue_backends/lite_backend.rb +42 -7
  24. data/lib/tina4/queue_backends/mongo_backend.rb +126 -0
  25. data/lib/tina4/rack_app.rb +200 -11
  26. data/lib/tina4/request.rb +14 -1
  27. data/lib/tina4/response.rb +26 -0
  28. data/lib/tina4/response_cache.rb +446 -29
  29. data/lib/tina4/router.rb +127 -0
  30. data/lib/tina4/service_runner.rb +1 -1
  31. data/lib/tina4/session.rb +6 -1
  32. data/lib/tina4/session_handlers/database_handler.rb +66 -0
  33. data/lib/tina4/swagger.rb +1 -1
  34. data/lib/tina4/templates/errors/404.twig +2 -2
  35. data/lib/tina4/templates/errors/500.twig +1 -1
  36. data/lib/tina4/validator.rb +174 -0
  37. data/lib/tina4/version.rb +1 -1
  38. data/lib/tina4/websocket.rb +23 -4
  39. data/lib/tina4/websocket_backplane.rb +118 -0
  40. data/lib/tina4.rb +126 -5
  41. metadata +40 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47c53ad8cda483d0cde2eaec4072b5620de88dbd172d45fe9ab0df52a865c71e
4
- data.tar.gz: 438bbf58dc14e8b7f91007cde01c53bc4326b4b088c8d39fac845189da9a2cf9
3
+ metadata.gz: 101a34de44fea572c9c21c8fea286f7adfb4c34fa5227be1c82b7def86ec8d71
4
+ data.tar.gz: d86ffef03c13ef707952678dde2961f9b430f81a81d7d618dd2a3c4811295611
5
5
  SHA512:
6
- metadata.gz: ae5246539beea74da1618b9e498d6f6f1ea3949e26663ec862d79934c3acdb6ec7ba59cb49778444badcab81aaa5a42de3e8e2c02ed4717d3ba368f0701045a7
7
- data.tar.gz: ddecbcd770270106b44733563e9c83f58e5b3084a0b190f3b5ea8041066f310cfec6017ee5e0fa03895c048a7a61100f9a8bca222a23825794726fdc7c1bebad
6
+ metadata.gz: 4446bf6ba80cdfb3d2ab17dabd523641f26a9eb09dbbff3cb9b6397e501206afa18bf85c1fe7b147d2aa0910ad2ca225322d7329cc6e3022ad493daee2f0e5a6
7
+ data.tar.gz: d4a7c8481f75d1fce92031da88b72ac88eda029aef5ebd417010c5545429b68c8f4fffd7a9048565df0e4512bfe6a23d3a578b4b952211ef2147978c18ca31ab
data/README.md CHANGED
@@ -9,6 +9,14 @@
9
9
  Laravel joy. Ruby speed. 10x less code. Zero third-party dependencies.
10
10
  </p>
11
11
 
12
+ <p align="center">
13
+ <a href="https://rubygems.org/gems/tina4"><img src="https://img.shields.io/gem/v/tina4?color=7b1fa2&label=RubyGems" alt="Gem"></a>
14
+ <img src="https://img.shields.io/badge/tests-1%2C578%20passing-brightgreen" alt="Tests">
15
+ <img src="https://img.shields.io/badge/features-38-blue" alt="Features">
16
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
17
+ <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
18
+ </p>
19
+
12
20
  <p align="center">
13
21
  <a href="https://tina4.com">Documentation</a> &bull;
14
22
  <a href="#getting-started">Getting Started</a> &bull;
@@ -17,27 +25,55 @@
17
25
  <a href="https://tina4.com">tina4.com</a>
18
26
  </p>
19
27
 
20
- <p align="center">
21
- <img src="https://img.shields.io/badge/tests-1334%20passing-brightgreen" alt="Tests">
22
- <img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
23
- <img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
24
- <img src="https://img.shields.io/badge/ruby-3.1%2B-blue" alt="Ruby 3.1+">
25
- <img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT License">
26
- </p>
27
-
28
28
  ---
29
29
 
30
- ## Quickstart
30
+ ## Quick Start
31
31
 
32
32
  ```bash
33
- gem install tina4ruby
34
- tina4ruby init my-app
35
- cd my-app
36
- tina4ruby serve
37
- # -> http://localhost:7147
33
+ # Install the Tina4 CLI
34
+ cargo install tina4 # or download binary from https://github.com/tina4stack/tina4/releases
35
+
36
+ # Create a project
37
+ tina4 init ruby ./my-app
38
+
39
+ # Run it
40
+ cd my-app && tina4 serve
38
41
  ```
39
42
 
40
- That's it. Zero configuration, zero classes, zero boilerplate.
43
+ Open http://localhost:7147 your app is running.
44
+
45
+ <details>
46
+ <summary><strong>Without the Tina4 CLI</strong></summary>
47
+
48
+ ```bash
49
+ # 1. Create project
50
+ mkdir my-app && cd my-app
51
+ echo 'source "https://rubygems.org"' > Gemfile
52
+ echo 'gem "tina4-ruby", "~> 3.0"' >> Gemfile
53
+ bundle install
54
+
55
+ # 2. Create entry point
56
+ cat > app.rb << 'EOF'
57
+ require "tina4"
58
+ Tina4.initialize!(__dir__)
59
+ app = Tina4::RackApp.new
60
+ Tina4::WebServer.new(app, host: "0.0.0.0", port: 7147).start
61
+ EOF
62
+
63
+ # 3. Create .env
64
+ echo 'TINA4_DEBUG=true' > .env
65
+ echo 'TINA4_LOG_LEVEL=ALL' >> .env
66
+
67
+ # 4. Create route directory
68
+ mkdir -p src/routes
69
+
70
+ # 5. Run
71
+ bundle exec ruby app.rb
72
+ ```
73
+
74
+ Open http://localhost:7147
75
+
76
+ </details>
41
77
 
42
78
  ---
43
79
 
@@ -50,17 +86,17 @@ Every feature is built from scratch -- no gem install, no node_modules, no third
50
86
  | **HTTP** | Rack 3 server, block routing, path params (`{id:int}`, `{p:path}`), middleware pipeline, CORS, rate limiting, graceful shutdown |
51
87
  | **Templates** | Frond engine (Twig-compatible), inheritance, partials, 35+ filters, macros, fragment caching, sandboxing |
52
88
  | **ORM** | Active Record, typed fields with validation, soft delete, relationships (`has_one`/`has_many`/`belongs_to`), scopes, result caching, multi-database |
53
- | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface |
54
- | **Auth** | Zero-dep JWT (RS256), sessions (file, Redis, MongoDB), password hashing, form tokens |
55
- | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE |
56
- | **Background** | DB-backed queue with priority, delayed jobs, retry, batch processing, multi-queue |
89
+ | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
90
+ | **Auth** | Zero-dep JWT (HS256/RS256), sessions (file/Redis/Valkey/MongoDB/database), password hashing, form tokens |
91
+ | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE, WSDL/SOAP with auto WSDL |
92
+ | **Background** | Queue (SQLite/RabbitMQ/Kafka/MongoDB) with priority, delayed jobs, retry, batch processing |
57
93
  | **Real-time** | Native WebSocket (RFC 6455), per-path routing, connection manager |
58
94
  | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
59
95
  | **DX** | Dev admin dashboard, error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
60
96
  | **Data** | Migrations with rollback, 50+ fake data generators, ORM and table seeders |
61
- | **Other** | REST client, localization (6 languages), in-memory cache (TTL/tags/LRU), event system, inline testing, configurable error pages |
97
+ | **Other** | REST client, localization (6 languages), cache (memory/Redis/file), event system, inline testing, messenger (.env driven), configurable error pages |
62
98
 
63
- **676 tests across 28 modules. All Carbonah benchmarks rated A+.**
99
+ **1,578 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
64
100
 
65
101
  For full documentation visit **[tina4.com](https://tina4.com)**.
66
102
 
@@ -322,7 +358,7 @@ db = Tina4::Database.new("mysql://localhost:3306/mydb", username: "user", passwo
322
358
  db = Tina4::Database.new("mssql://localhost:1433/mydb", username: "sa", password: "pass")
323
359
  db = Tina4::Database.new("firebird://localhost:3050/path/to/db", username: "SYSDBA", password: "masterkey")
324
360
 
325
- result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit: 20, skip: 0)
361
+ result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit: 20, offset: 0)
326
362
  row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
327
363
  db.insert("users", { name: "Alice", email: "alice@test.com" })
328
364
  db.commit
@@ -345,9 +381,9 @@ end
345
381
  ### JWT Authentication
346
382
 
347
383
  ```ruby
348
- auth = Tina4::Auth.new
349
- token = auth.get_token({ user_id: 42 })
350
- payload = auth.get_payload(token)
384
+ token = Tina4::Auth.get_token({ user_id: 42 })
385
+ result = Tina4::Auth.valid_token(token)
386
+ payload = Tina4::Auth.get_payload(token)
351
387
  ```
352
388
 
353
389
  POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `auth: false` to make public, `secure_get` to protect GET routes.
@@ -364,11 +400,10 @@ Backends: file (default), Redis, MongoDB. Set via `TINA4_SESSION_HANDLER` in `.e
364
400
  ### Queues
365
401
 
366
402
  ```ruby
367
- producer = Tina4::Producer.new(Tina4::Queue.new(topic: "emails"))
368
- producer.produce({ to: "alice@example.com" })
403
+ queue = Tina4::Queue.new(topic: "emails")
404
+ queue.produce("emails", { to: "alice@example.com" })
369
405
 
370
- consumer = Tina4::Consumer.new(Tina4::Queue.new(topic: "emails"))
371
- consumer.each { |msg| send_email(msg.data) }
406
+ queue.consume("emails") { |msg| send_email(msg.payload) }
372
407
  ```
373
408
 
374
409
  ### GraphQL
@@ -486,7 +521,7 @@ cache.tag("users").flush
486
521
 
487
522
  ## Dev Mode
488
523
 
489
- Set `TINA4_DEBUG_LEVEL=DEBUG` in `.env` to enable:
524
+ Set `TINA4_DEBUG=true` in `.env` to enable:
490
525
 
491
526
  - **Live reload** -- browser auto-refreshes on code changes
492
527
  - **CSS hot-reload** -- SCSS changes apply without page refresh
@@ -500,9 +535,14 @@ Set `TINA4_DEBUG_LEVEL=DEBUG` in `.env` to enable:
500
535
  ```bash
501
536
  tina4ruby init [dir] # Scaffold a new project
502
537
  tina4ruby serve [port] # Start dev server (default: 7147)
538
+ tina4ruby serve --production # Auto-install and use Puma production server
503
539
  tina4ruby migrate # Run pending migrations
504
540
  tina4ruby migrate --create <desc># Create a migration file
505
541
  tina4ruby migrate --rollback # Rollback last batch
542
+ tina4ruby generate model <name> # Generate ORM model scaffold
543
+ tina4ruby generate route <name> # Generate route scaffold
544
+ tina4ruby generate migration <d> # Generate migration file
545
+ tina4ruby generate middleware <n># Generate middleware scaffold
506
546
  tina4ruby seed # Run seeders from src/seeds/
507
547
  tina4ruby routes # List all registered routes
508
548
  tina4ruby test # Run test suite
@@ -510,6 +550,53 @@ tina4ruby build # Build distributable gem
510
550
  tina4ruby ai [--all] # Detect AI tools and install context
511
551
  ```
512
552
 
553
+ ### Production Server Auto-Detection
554
+
555
+ `tina4 serve` automatically detects and uses the best available production server:
556
+
557
+ - **Ruby**: Puma (if installed), otherwise WEBrick -- Puma gives 2.8x improvement
558
+ - Use `tina4ruby serve --production` to auto-install Puma
559
+
560
+ ### Scaffolding with `tina4 generate`
561
+
562
+ Quickly scaffold new components:
563
+
564
+ ```bash
565
+ tina4ruby generate model User # Creates src/orm/user.rb with field stubs
566
+ tina4ruby generate route users # Creates src/routes/users.rb with CRUD stubs
567
+ tina4ruby generate migration "add age" # Creates migration SQL file
568
+ tina4ruby generate middleware AuthLog # Creates middleware class
569
+ ```
570
+
571
+ ### ORM Relationships & Eager Loading
572
+
573
+ ```ruby
574
+ # Relationships
575
+ orders = user.has_many("Order", "user_id")
576
+ profile = user.has_one("Profile", "user_id")
577
+ customer = order.belongs_to("Customer", "customer_id")
578
+
579
+ # Eager loading with include:
580
+ users = User.all(include: ["orders", "profile"])
581
+ ```
582
+
583
+ ### DB Query Caching
584
+
585
+ Enable query caching for up to 4x speedup on read-heavy workloads:
586
+
587
+ ```bash
588
+ # .env
589
+ TINA4_DB_CACHE=true
590
+ ```
591
+
592
+ ### Frond Pre-Compilation
593
+
594
+ Templates are pre-compiled for 2.8x faster rendering.
595
+
596
+ ### Gallery
597
+
598
+ 7 interactive examples with **Try It** deploy.
599
+
513
600
  ## Environment
514
601
 
515
602
  ```bash
@@ -517,8 +604,9 @@ SECRET=your-jwt-secret
517
604
  DATABASE_URL=sqlite://data/app.db
518
605
  DATABASE_USERNAME=
519
606
  DATABASE_PASSWORD=
520
- TINA4_DEBUG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR, ALL
521
- TINA4_LANGUAGE=en # en, fr, af, zh, ja, es
607
+ TINA4_DEBUG=true # Enable dev toolbar, error overlay
608
+ TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
609
+ TINA4_LOCALE=en # en, fr, af, zh, ja, es
522
610
  TINA4_SESSION_HANDLER=SessionFileHandler
523
611
  SWAGGER_TITLE=My API
524
612
  ```
data/lib/tina4/auth.rb CHANGED
@@ -12,27 +12,130 @@ module Tina4
12
12
  def setup(root_dir = Dir.pwd)
13
13
  @keys_dir = File.join(root_dir, KEYS_DIR)
14
14
  FileUtils.mkdir_p(@keys_dir)
15
- ensure_keys
15
+ ensure_keys unless use_hmac?
16
16
  end
17
17
 
18
- def create_token(payload, expires_in: 3600)
19
- ensure_keys
18
+ # ── HS256 helpers (stdlib only, no gem) ──────────────────────
19
+
20
+ # Returns true when SECRET env var is set and no RSA keys exist in .keys/
21
+ def use_hmac?
22
+ secret = ENV["SECRET"]
23
+ return false if secret.nil? || secret.empty?
24
+
25
+ # If RSA keys already exist on disk, prefer RS256 for backward compat
26
+ @keys_dir ||= File.join(Dir.pwd, KEYS_DIR)
27
+ !(File.exist?(File.join(@keys_dir, "private.pem")) &&
28
+ File.exist?(File.join(@keys_dir, "public.pem")))
29
+ end
30
+
31
+ def hmac_secret
32
+ ENV["SECRET"]
33
+ end
34
+
35
+ # Base64url-encode without padding (JWT spec)
36
+ def base64url_encode(data)
37
+ Base64.urlsafe_encode64(data, padding: false)
38
+ end
39
+
40
+ # Base64url-decode (handles missing padding)
41
+ def base64url_decode(str)
42
+ # Add back padding
43
+ remainder = str.length % 4
44
+ str += "=" * ((4 - remainder) % 4) if remainder != 0
45
+ Base64.urlsafe_decode64(str)
46
+ end
47
+
48
+ # Build a JWT using HS256 with Ruby's OpenSSL::HMAC (no gem needed)
49
+ def hmac_encode(claims, secret)
50
+ header = { "alg" => "HS256", "typ" => "JWT" }
51
+ segments = [
52
+ base64url_encode(JSON.generate(header)),
53
+ base64url_encode(JSON.generate(claims))
54
+ ]
55
+ signing_input = segments.join(".")
56
+ signature = OpenSSL::HMAC.digest("SHA256", secret, signing_input)
57
+ segments << base64url_encode(signature)
58
+ segments.join(".")
59
+ end
60
+
61
+ # Decode and verify a JWT signed with HS256. Returns the payload hash or nil.
62
+ def hmac_decode(token, secret)
63
+ parts = token.split(".")
64
+ return nil unless parts.length == 3
65
+
66
+ header_json = base64url_decode(parts[0])
67
+ header = JSON.parse(header_json)
68
+ return nil unless header["alg"] == "HS256"
69
+
70
+ # Verify signature
71
+ signing_input = "#{parts[0]}.#{parts[1]}"
72
+ expected_sig = OpenSSL::HMAC.digest("SHA256", secret, signing_input)
73
+ actual_sig = base64url_decode(parts[2])
74
+
75
+ # Constant-time comparison to prevent timing attacks
76
+ return nil unless OpenSSL.fixed_length_secure_compare(expected_sig, actual_sig)
77
+
78
+ payload = JSON.parse(base64url_decode(parts[1]))
79
+
80
+ # Check expiry
81
+ now = Time.now.to_i
82
+ return nil if payload["exp"] && now >= payload["exp"]
83
+ return nil if payload["nbf"] && now < payload["nbf"]
84
+
85
+ payload
86
+ rescue ArgumentError, JSON::ParserError, OpenSSL::HMACError
87
+ nil
88
+ end
89
+
90
+ # ── Token API (auto-selects HS256 or RS256) ─────────────────
91
+
92
+ def get_token(payload, expires_in: 3600)
20
93
  now = Time.now.to_i
21
94
  claims = payload.merge(
22
95
  "iat" => now,
23
96
  "exp" => now + expires_in,
24
97
  "nbf" => now
25
98
  )
26
- require "jwt"
27
- JWT.encode(claims, private_key, "RS256")
99
+
100
+ if use_hmac?
101
+ hmac_encode(claims, hmac_secret)
102
+ else
103
+ ensure_keys
104
+ require "jwt"
105
+ JWT.encode(claims, private_key, "RS256")
106
+ end
28
107
  end
29
108
 
30
109
 
31
- def validate_token(token)
32
- ensure_keys
33
- require "jwt"
34
- decoded = JWT.decode(token, public_key, true, algorithm: "RS256")
35
- { valid: true, payload: decoded[0] }
110
+ def valid_token(token)
111
+ if use_hmac?
112
+ hmac_decode(token, hmac_secret)
113
+ else
114
+ ensure_keys
115
+ require "jwt"
116
+ decoded = JWT.decode(token, public_key, true, algorithm: "RS256")
117
+ decoded[0]
118
+ end
119
+ rescue JWT::ExpiredSignature
120
+ nil
121
+ rescue JWT::DecodeError
122
+ nil
123
+ end
124
+
125
+ def valid_token_detail(token)
126
+ if use_hmac?
127
+ payload = hmac_decode(token, hmac_secret)
128
+ if payload
129
+ { valid: true, payload: payload }
130
+ else
131
+ { valid: false, error: "Invalid or expired token" }
132
+ end
133
+ else
134
+ ensure_keys
135
+ require "jwt"
136
+ decoded = JWT.decode(token, public_key, true, algorithm: "RS256")
137
+ { valid: true, payload: decoded[0] }
138
+ end
36
139
  rescue JWT::ExpiredSignature
37
140
  { valid: false, error: "Token expired" }
38
141
  rescue JWT::DecodeError => e
@@ -53,34 +156,36 @@ module Tina4
53
156
 
54
157
 
55
158
  def get_payload(token)
56
- require "jwt"
57
- decoded = JWT.decode(token, nil, false)
58
- decoded[0]
59
- rescue JWT::DecodeError
159
+ parts = token.split(".")
160
+ return nil unless parts.length == 3
161
+
162
+ payload_json = base64url_decode(parts[1])
163
+ JSON.parse(payload_json)
164
+ rescue ArgumentError, JSON::ParserError
60
165
  nil
61
166
  end
62
167
 
63
168
  def refresh_token(token, expires_in: 3600)
64
- result = validate_token(token)
65
- return nil unless result[:valid]
169
+ payload = valid_token(token)
170
+ return nil unless payload
66
171
 
67
- payload = result[:payload].reject { |k, _| %w[iat exp nbf].include?(k) }
68
- create_token(payload, expires_in: expires_in)
172
+ payload = payload.reject { |k, _| %w[iat exp nbf].include?(k) }
173
+ get_token(payload, expires_in: expires_in)
69
174
  end
70
175
 
71
176
  def authenticate_request(headers)
72
177
  auth_header = headers["HTTP_AUTHORIZATION"] || headers["Authorization"] || ""
73
- return { valid: false, error: "No authorization header" } unless auth_header =~ /\ABearer\s+(.+)\z/i
178
+ return nil unless auth_header =~ /\ABearer\s+(.+)\z/i
74
179
 
75
180
  token = Regexp.last_match(1)
76
181
 
77
182
  # API_KEY bypass — matches tina4_python behavior
78
183
  api_key = ENV["TINA4_API_KEY"] || ENV["API_KEY"]
79
184
  if api_key && !api_key.empty? && token == api_key
80
- return { valid: true, payload: { "api_key" => true } }
185
+ return { "api_key" => true }
81
186
  end
82
187
 
83
- validate_token(token)
188
+ valid_token(token)
84
189
  end
85
190
 
86
191
  def validate_api_key(provided, expected: nil)
@@ -107,15 +212,15 @@ module Tina4
107
212
  token = Regexp.last_match(1)
108
213
 
109
214
  # API_KEY bypass — matches tina4_python behavior
110
- api_key = ENV["API_KEY"]
215
+ api_key = ENV["TINA4_API_KEY"] || ENV["API_KEY"]
111
216
  if api_key && !api_key.empty? && token == api_key
112
217
  env["tina4.auth"] = { "api_key" => true }
113
218
  return true
114
219
  end
115
220
 
116
- result = validate_token(token)
117
- if result[:valid]
118
- env["tina4.auth"] = result[:payload]
221
+ payload = valid_token(token)
222
+ if payload
223
+ env["tina4.auth"] = payload
119
224
  true
120
225
  else
121
226
  false
@@ -129,6 +234,10 @@ module Tina4
129
234
  @default_secure_auth ||= bearer_auth
130
235
  end
131
236
 
237
+ # Legacy aliases
238
+ alias_method :create_token, :get_token
239
+ alias_method :validate_token, :valid_token_detail
240
+
132
241
  def private_key
133
242
  @private_key ||= OpenSSL::PKey::RSA.new(File.read(private_key_path))
134
243
  end
@@ -140,6 +249,8 @@ module Tina4
140
249
  private
141
250
 
142
251
  def ensure_keys
252
+ return if use_hmac?
253
+
143
254
  @keys_dir ||= File.join(Dir.pwd, KEYS_DIR)
144
255
  FileUtils.mkdir_p(@keys_dir)
145
256
  unless File.exist?(private_key_path) && File.exist?(public_key_path)
@@ -169,8 +280,7 @@ module Tina4
169
280
  return true if auth_header.empty?
170
281
 
171
282
  if auth_header =~ /\ABearer\s+(.+)\z/i
172
- result = validate_token(Regexp.last_match(1))
173
- result[:valid]
283
+ !valid_token(Regexp.last_match(1)).nil?
174
284
  else
175
285
  false
176
286
  end
@@ -21,10 +21,36 @@ module Tina4
21
21
  end
22
22
  end
23
23
 
24
+ # Build a sample request body from ORM field definitions.
25
+ def build_example(model_class)
26
+ example = {}
27
+ return example unless model_class.respond_to?(:field_definitions)
28
+
29
+ model_class.field_definitions.each do |name, opts|
30
+ next if opts[:primary_key] && opts[:auto_increment]
31
+
32
+ case opts[:type]
33
+ when :integer
34
+ example[name.to_s] = 0
35
+ when :numeric, :float, :decimal
36
+ example[name.to_s] = 0.0
37
+ when :boolean
38
+ example[name.to_s] = true
39
+ when :datetime
40
+ example[name.to_s] = "2024-01-01T00:00:00"
41
+ else
42
+ example[name.to_s] = "string"
43
+ end
44
+ end
45
+ example
46
+ end
47
+
24
48
  # Generate REST endpoints for a single model class
25
49
  def generate_routes_for(model_class, prefix: "/api")
26
50
  table = model_class.table_name
27
51
  pk = model_class.primary_key_field || :id
52
+ pretty_name = table.to_s.split("_").map(&:capitalize).join(" ")
53
+ example_body = build_example(model_class)
28
54
 
29
55
  # GET /api/{table} -- list all with pagination, filtering, sorting
30
56
  Tina4::Router.add_route("GET", "#{prefix}/#{table}", proc { |req, res|
@@ -63,7 +89,7 @@ module Tina4
63
89
  rescue => e
64
90
  res.json({ error: e.message }, status: 500)
65
91
  end
66
- })
92
+ }, swagger_meta: { summary: "List all #{pretty_name}", tags: [table.to_s] })
67
93
 
68
94
  # GET /api/{table}/{id} -- get single record
69
95
  Tina4::Router.add_route("GET", "#{prefix}/#{table}/{id}", proc { |req, res|
@@ -78,7 +104,7 @@ module Tina4
78
104
  rescue => e
79
105
  res.json({ error: e.message }, status: 500)
80
106
  end
81
- })
107
+ }, swagger_meta: { summary: "Get #{pretty_name} by ID", tags: [table.to_s] })
82
108
 
83
109
  # POST /api/{table} -- create record
84
110
  Tina4::Router.add_route("POST", "#{prefix}/#{table}", proc { |req, res|
@@ -93,6 +119,19 @@ module Tina4
93
119
  rescue => e
94
120
  res.json({ error: e.message }, status: 500)
95
121
  end
122
+ }, swagger_meta: {
123
+ summary: "Create #{pretty_name}",
124
+ tags: [table.to_s],
125
+ request_body: {
126
+ "description" => "#{pretty_name} data",
127
+ "required" => true,
128
+ "content" => {
129
+ "application/json" => {
130
+ "schema" => { "type" => "object" },
131
+ "example" => example_body
132
+ }
133
+ }
134
+ }
96
135
  })
97
136
 
98
137
  # PUT /api/{table}/{id} -- update record
@@ -118,6 +157,19 @@ module Tina4
118
157
  rescue => e
119
158
  res.json({ error: e.message }, status: 500)
120
159
  end
160
+ }, swagger_meta: {
161
+ summary: "Update #{pretty_name}",
162
+ tags: [table.to_s],
163
+ request_body: {
164
+ "description" => "#{pretty_name} data",
165
+ "required" => true,
166
+ "content" => {
167
+ "application/json" => {
168
+ "schema" => { "type" => "object" },
169
+ "example" => example_body
170
+ }
171
+ }
172
+ }
121
173
  })
122
174
 
123
175
  # DELETE /api/{table}/{id} -- delete record
@@ -137,7 +189,7 @@ module Tina4
137
189
  rescue => e
138
190
  res.json({ error: e.message }, status: 500)
139
191
  end
140
- })
192
+ }, swagger_meta: { summary: "Delete #{pretty_name}", tags: [table.to_s] })
141
193
  end
142
194
 
143
195
  def clear!