tina4ruby 3.11.13 → 3.11.15
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 +4 -4
- data/CHANGELOG.md +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/rate_limiter.rb
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
class RateLimiter
|
|
5
|
-
DEFAULT_LIMIT = 100
|
|
6
|
-
DEFAULT_WINDOW = 60 # seconds
|
|
7
|
-
|
|
8
|
-
attr_reader :limit, :window
|
|
9
|
-
|
|
10
|
-
def initialize(limit: nil, window: nil)
|
|
11
|
-
@limit = (limit || ENV["TINA4_RATE_LIMIT"] || DEFAULT_LIMIT).to_i
|
|
12
|
-
@window = (window || ENV["TINA4_RATE_WINDOW"] || DEFAULT_WINDOW).to_i
|
|
13
|
-
@store = {} # ip => [timestamps]
|
|
14
|
-
@mutex = Mutex.new
|
|
15
|
-
@last_cleanup = Time.now
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Check if the given IP is rate limited.
|
|
19
|
-
# Returns a hash with rate limit info:
|
|
20
|
-
# { allowed: true/false, limit:, remaining:, reset:, retry_after: }
|
|
21
|
-
def check(ip)
|
|
22
|
-
now = Time.now
|
|
23
|
-
cleanup_if_needed(now)
|
|
24
|
-
|
|
25
|
-
@mutex.synchronize do
|
|
26
|
-
@store[ip] ||= []
|
|
27
|
-
entries = @store[ip]
|
|
28
|
-
|
|
29
|
-
# Remove expired entries (sliding window)
|
|
30
|
-
cutoff = now - @window
|
|
31
|
-
entries.reject! { |t| t < cutoff }
|
|
32
|
-
|
|
33
|
-
if entries.length >= @limit
|
|
34
|
-
# Rate limited
|
|
35
|
-
oldest = entries.first
|
|
36
|
-
reset_at = (oldest + @window).to_i
|
|
37
|
-
retry_after = [(oldest + @window - now).ceil, 1].max
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
allowed: false,
|
|
41
|
-
limit: @limit,
|
|
42
|
-
remaining: 0,
|
|
43
|
-
reset: reset_at,
|
|
44
|
-
retry_after: retry_after
|
|
45
|
-
}
|
|
46
|
-
else
|
|
47
|
-
entries << now
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
allowed: true,
|
|
51
|
-
limit: @limit,
|
|
52
|
-
remaining: @limit - entries.length,
|
|
53
|
-
reset: (now + @window).to_i,
|
|
54
|
-
retry_after: nil
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Convenience predicate
|
|
61
|
-
def rate_limited?(ip)
|
|
62
|
-
!check(ip)[:allowed]
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Apply rate limit headers to a response object and return 429 if exceeded.
|
|
66
|
-
# Returns [status, headers_hash] or nil if allowed.
|
|
67
|
-
def apply(ip, response)
|
|
68
|
-
result = check(ip)
|
|
69
|
-
|
|
70
|
-
# Always set rate limit headers
|
|
71
|
-
response.headers["X-RateLimit-Limit"] = result[:limit].to_s
|
|
72
|
-
response.headers["X-RateLimit-Remaining"] = result[:remaining].to_s
|
|
73
|
-
response.headers["X-RateLimit-Reset"] = result[:reset].to_s
|
|
74
|
-
|
|
75
|
-
unless result[:allowed]
|
|
76
|
-
response.headers["Retry-After"] = result[:retry_after].to_s
|
|
77
|
-
response.status_code = 429
|
|
78
|
-
response.headers["content-type"] = "application/json; charset=utf-8"
|
|
79
|
-
response.body = JSON.generate({
|
|
80
|
-
error: "Too Many Requests",
|
|
81
|
-
retry_after: result[:retry_after]
|
|
82
|
-
})
|
|
83
|
-
return false
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
true
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Standardized middleware hook — enforces rate limiting before the route handler.
|
|
90
|
-
def before_rate_limit(request, response)
|
|
91
|
-
ip = request.respond_to?(:ip) ? request.ip : "unknown"
|
|
92
|
-
apply(ip, response)
|
|
93
|
-
[request, response]
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Reset tracking for a specific IP (useful for testing)
|
|
97
|
-
def reset(ip = nil)
|
|
98
|
-
@mutex.synchronize do
|
|
99
|
-
if ip
|
|
100
|
-
@store.delete(ip)
|
|
101
|
-
else
|
|
102
|
-
@store.clear
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Returns current entry count (for monitoring)
|
|
108
|
-
def entry_count
|
|
109
|
-
@mutex.synchronize { @store.length }
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
private
|
|
113
|
-
|
|
114
|
-
# Clean up expired entries periodically (every window interval)
|
|
115
|
-
def cleanup_if_needed(now)
|
|
116
|
-
return if now - @last_cleanup < @window
|
|
117
|
-
|
|
118
|
-
@mutex.synchronize do
|
|
119
|
-
return if now - @last_cleanup < @window
|
|
120
|
-
|
|
121
|
-
cutoff = now - @window
|
|
122
|
-
@store.delete_if do |_ip, entries|
|
|
123
|
-
entries.reject! { |t| t < cutoff }
|
|
124
|
-
entries.empty?
|
|
125
|
-
end
|
|
126
|
-
@last_cleanup = now
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
class RateLimiter
|
|
5
|
+
DEFAULT_LIMIT = 100
|
|
6
|
+
DEFAULT_WINDOW = 60 # seconds
|
|
7
|
+
|
|
8
|
+
attr_reader :limit, :window
|
|
9
|
+
|
|
10
|
+
def initialize(limit: nil, window: nil)
|
|
11
|
+
@limit = (limit || ENV["TINA4_RATE_LIMIT"] || DEFAULT_LIMIT).to_i
|
|
12
|
+
@window = (window || ENV["TINA4_RATE_WINDOW"] || DEFAULT_WINDOW).to_i
|
|
13
|
+
@store = {} # ip => [timestamps]
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@last_cleanup = Time.now
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Check if the given IP is rate limited.
|
|
19
|
+
# Returns a hash with rate limit info:
|
|
20
|
+
# { allowed: true/false, limit:, remaining:, reset:, retry_after: }
|
|
21
|
+
def check(ip)
|
|
22
|
+
now = Time.now
|
|
23
|
+
cleanup_if_needed(now)
|
|
24
|
+
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
@store[ip] ||= []
|
|
27
|
+
entries = @store[ip]
|
|
28
|
+
|
|
29
|
+
# Remove expired entries (sliding window)
|
|
30
|
+
cutoff = now - @window
|
|
31
|
+
entries.reject! { |t| t < cutoff }
|
|
32
|
+
|
|
33
|
+
if entries.length >= @limit
|
|
34
|
+
# Rate limited
|
|
35
|
+
oldest = entries.first
|
|
36
|
+
reset_at = (oldest + @window).to_i
|
|
37
|
+
retry_after = [(oldest + @window - now).ceil, 1].max
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
allowed: false,
|
|
41
|
+
limit: @limit,
|
|
42
|
+
remaining: 0,
|
|
43
|
+
reset: reset_at,
|
|
44
|
+
retry_after: retry_after
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
entries << now
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
allowed: true,
|
|
51
|
+
limit: @limit,
|
|
52
|
+
remaining: @limit - entries.length,
|
|
53
|
+
reset: (now + @window).to_i,
|
|
54
|
+
retry_after: nil
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convenience predicate
|
|
61
|
+
def rate_limited?(ip)
|
|
62
|
+
!check(ip)[:allowed]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Apply rate limit headers to a response object and return 429 if exceeded.
|
|
66
|
+
# Returns [status, headers_hash] or nil if allowed.
|
|
67
|
+
def apply(ip, response)
|
|
68
|
+
result = check(ip)
|
|
69
|
+
|
|
70
|
+
# Always set rate limit headers
|
|
71
|
+
response.headers["X-RateLimit-Limit"] = result[:limit].to_s
|
|
72
|
+
response.headers["X-RateLimit-Remaining"] = result[:remaining].to_s
|
|
73
|
+
response.headers["X-RateLimit-Reset"] = result[:reset].to_s
|
|
74
|
+
|
|
75
|
+
unless result[:allowed]
|
|
76
|
+
response.headers["Retry-After"] = result[:retry_after].to_s
|
|
77
|
+
response.status_code = 429
|
|
78
|
+
response.headers["content-type"] = "application/json; charset=utf-8"
|
|
79
|
+
response.body = JSON.generate({
|
|
80
|
+
error: "Too Many Requests",
|
|
81
|
+
retry_after: result[:retry_after]
|
|
82
|
+
})
|
|
83
|
+
return false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Standardized middleware hook — enforces rate limiting before the route handler.
|
|
90
|
+
def before_rate_limit(request, response)
|
|
91
|
+
ip = request.respond_to?(:ip) ? request.ip : "unknown"
|
|
92
|
+
apply(ip, response)
|
|
93
|
+
[request, response]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Reset tracking for a specific IP (useful for testing)
|
|
97
|
+
def reset(ip = nil)
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
if ip
|
|
100
|
+
@store.delete(ip)
|
|
101
|
+
else
|
|
102
|
+
@store.clear
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns current entry count (for monitoring)
|
|
108
|
+
def entry_count
|
|
109
|
+
@mutex.synchronize { @store.length }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Clean up expired entries periodically (every window interval)
|
|
115
|
+
def cleanup_if_needed(now)
|
|
116
|
+
return if now - @last_cleanup < @window
|
|
117
|
+
|
|
118
|
+
@mutex.synchronize do
|
|
119
|
+
return if now - @last_cleanup < @window
|
|
120
|
+
|
|
121
|
+
cutoff = now - @window
|
|
122
|
+
@store.delete_if do |_ip, entries|
|
|
123
|
+
entries.reject! { |t| t < cutoff }
|
|
124
|
+
entries.empty?
|
|
125
|
+
end
|
|
126
|
+
@last_cleanup = now
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|