Class: GRApiManager::Server
- Inherits:
-
Object
- Object
- GRApiManager::Server
- Defined in:
- lib/gr_api_manager.rb
Overview
Server — the public-facing DSL.
Instance Attribute Summary collapse
-
#app_class ⇒ Object
readonly
Returns the value of attribute app_class.
-
#jwt_secret ⇒ Object
readonly
Returns the value of attribute jwt_secret.
-
#rate_limiter ⇒ Object
readonly
Returns the value of attribute rate_limiter.
Instance Method Summary collapse
-
#group(prefix = '', options = {}, &block) ⇒ Object
Groups routes under a common prefix with inherited options.
-
#initialize(port: nil, bearer_token: nil, jwt_secret: nil, permitted_hosts: [], prefix: '', max_body_size: GRApiManager.mb(50), dev_mode: false, rate_limit: nil, rate_limit_window: 60, rate_limit_store: nil, trust_proxy_headers: true) ⇒ Server
constructor
Initializes the server configuration.
-
#jwt_decode(token) ⇒ Object
Decodes a JWT token using the configured jwt_secret.
-
#jwt_encode(payload, exp: nil) ⇒ Object
Encodes a payload into a JWT token using the configured jwt_secret.
-
#register_route(verb, path, options = {}, &block) ⇒ Object
Core routing logic: auth validation, body parsing, param merging, validation, execution.
-
#run!(workers: nil, threads: '2:8') ⇒ Object
Starts the Sinatra server.
Constructor Details
#initialize(port: nil, bearer_token: nil, jwt_secret: nil, permitted_hosts: [], prefix: '', max_body_size: GRApiManager.mb(50), dev_mode: false, rate_limit: nil, rate_limit_window: 60, rate_limit_store: nil, trust_proxy_headers: true) ⇒ Server
Initializes the server configuration.
Options:
port: Integer – listening port (default: ENV['PORT'] || 4000)
bearer_token: String – Bearer token for auth (default: ENV['API_TOKEN'])
jwt_secret: String – Secret key for signing/decoding JWTs (default: ENV['JWT_SECRET'])
permitted_hosts: Array – host allowlist; empty = allow all
prefix: String – route prefix, e.g. '/api/v1'
max_body_size: Integer – maximum accepted body in bytes (default: 50 MB)
dev_mode: Boolean – show full stack traces on 500 (default: false)
rate_limit: Integer – max requests per IP per window (nil = disabled)
rate_limit_window: Integer – sliding window in seconds (default: 60)
rate_limit_store: Object – custom store object (default: MemoryStore)
trust_proxy_headers: Boolean – inspect Cloudflare/X-Real-IP/X-Forwarded-For headers (default: true)
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 |
# File 'lib/gr_api_manager.rb', line 556 def initialize( port: nil, bearer_token: nil, jwt_secret: nil, permitted_hosts: [], prefix: '', max_body_size: GRApiManager.mb(50), dev_mode: false, rate_limit: nil, rate_limit_window: 60, rate_limit_store: nil, trust_proxy_headers: true ) @port = port || ENV['PORT'] || 4000 @token = bearer_token || ENV['API_TOKEN'] @jwt_secret = jwt_secret || ENV['JWT_SECRET'] @permitted_hosts = permitted_hosts.empty? ? [] : permitted_hosts @prefix = prefix @max_body_size = max_body_size @dev_mode = dev_mode @trust_proxy_headers = trust_proxy_headers @rate_limiter = rate_limit ? GRApiManager::RateLimiter.new( max_requests: rate_limit, window_seconds: rate_limit_window, store: rate_limit_store ) : nil @app_class = Class.new(Sinatra::Base) do # Logs HTTP requests with status-based color coding. def log_request(method, path, status_code) color = status_code.between?(200, 299) ? "\e[32m" : "\e[31m" puts "[#{Time.now.strftime('%H:%M:%S')}] #{color}#{method} #{path} - #{status_code}\e[0m" end # Casts string URL parameters to native Ruby types (Integer, Float, Boolean). def smart_parse(hash) hash.transform_values do |val| next val unless val.is_a?(String) case val when 'true' then true when 'false' then false when /^-?\d+$/ then val.to_i when /^-?\d+\.\d+$/ then val.to_f else val end end end end configure_app end |
Instance Attribute Details
#app_class ⇒ Object (readonly)
Returns the value of attribute app_class.
540 541 542 |
# File 'lib/gr_api_manager.rb', line 540 def app_class @app_class end |
#jwt_secret ⇒ Object (readonly)
Returns the value of attribute jwt_secret.
540 541 542 |
# File 'lib/gr_api_manager.rb', line 540 def jwt_secret @jwt_secret end |
#rate_limiter ⇒ Object (readonly)
Returns the value of attribute rate_limiter.
540 541 542 |
# File 'lib/gr_api_manager.rb', line 540 def rate_limiter @rate_limiter end |
Instance Method Details
#group(prefix = '', options = {}, &block) ⇒ Object
Groups routes under a common prefix with inherited options.
622 623 624 625 626 |
# File 'lib/gr_api_manager.rb', line 622 def group(prefix = '', = {}, &block) route_group = RouteGroup.new(self, prefix, ) block.call(route_group) if block route_group end |
#jwt_decode(token) ⇒ Object
Decodes a JWT token using the configured jwt_secret.
616 617 618 619 |
# File 'lib/gr_api_manager.rb', line 616 def jwt_decode(token) raise "No jwt_secret configured for this server" unless @jwt_secret GRApiManager::JWT.decode(token, @jwt_secret) end |
#jwt_encode(payload, exp: nil) ⇒ Object
Encodes a payload into a JWT token using the configured jwt_secret.
610 611 612 613 |
# File 'lib/gr_api_manager.rb', line 610 def jwt_encode(payload, exp: nil) raise "No jwt_secret configured for this server" unless @jwt_secret GRApiManager::JWT.encode(payload, @jwt_secret, exp: exp) end |
#register_route(verb, path, options = {}, &block) ⇒ Object
Core routing logic: auth validation, body parsing, param merging, validation, execution.
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 |
# File 'lib/gr_api_manager.rb', line 713 def register_route(verb, path, = {}, &block) verb_up = verb.to_s.upcase require_auth = .fetch(:auth, true) required_params = .fetch(:requires, nil) # Construct the full path with the optional prefix. full_path = File.join('/', @prefix.to_s, path.to_s).gsub(%r{/+}, '/') handler = proc do # 1. Authentication check jwt_user = nil if require_auth auth_header = request.env["HTTP_AUTHORIZATION"] halt 401, { error: "Token required. Format: 'Bearer <token>'" }.to_json if auth_header.nil? raw_token = auth_header.split(" ").last if require_auth == :jwt || (require_auth == true && settings.jwt_secret && settings.token.nil?) # JWT authentication mode halt 500, { error: "Server error: jwt_secret is not configured" }.to_json unless settings.jwt_secret begin jwt_user = GRApiManager::JWT.decode(raw_token, settings.jwt_secret) rescue GRApiManager::JWT::DecodeError => e halt 401, { error: "Invalid token: #{e.}" }.to_json end else # Static Bearer Token mode if raw_token != settings.token # Fallback: if jwt_secret is set, try JWT decoding if settings.jwt_secret begin jwt_user = GRApiManager::JWT.decode(raw_token, settings.jwt_secret) rescue GRApiManager::JWT::DecodeError halt 403, { error: "Invalid token" }.to_json end else halt 403, { error: "Invalid token" }.to_json end end end end # 2. Body parsing — smart detection based on Content-Type parsed_body = {} if %w[POST PUT PATCH].include?(verb_up) begin parsed_body = GRApiManager::BodyParser.parse(request) rescue ArgumentError => e halt 400, { error: e. }.to_json end end # 3. Merge query/path parameters with parsed body. url_params = smart_parse(params.reject { |_, v| v.is_a?(Hash) && v.key?(:tempfile) }) all_params = url_params.merge(parsed_body) # Inyect JWT payload if authenticated via JWT if jwt_user all_params[:current_user] = jwt_user all_params[:jwt_payload] = jwt_user end # 4. Declarative parameter validation (Array of keys or Hash schema) if required_params.is_a?(Hash) is_valid, errors = GRApiManager::Validator.validate(all_params, required_params) unless is_valid status 400 log_request(verb_up, full_path, 400) next { error: "Validation failed", errors: errors }.to_json end elsif required_params.is_a?(Array) && required_params.any? missing = required_params.select do |p| val = all_params[p.to_sym] val.nil? || (val.is_a?(String) && val.strip.empty?) end if missing.any? status 400 log_request(verb_up, full_path, 400) next { error: "Missing required parameters", required: missing }.to_json end end # 5. Execute user-defined block result = instance_exec(all_params, &block) log_request(verb_up, full_path, response.status) result.is_a?(String) ? result : result.to_json end @app_class.send(verb.downcase, full_path, &handler) end |
#run!(workers: nil, threads: '2:8') ⇒ Object
Starts the Sinatra server.
807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 |
# File 'lib/gr_api_manager.rb', line 807 def run!(workers: nil, threads: '2:8') w = (workers || ENV.fetch('WEB_CONCURRENCY', 2)).to_i min_t, max_t = threads.to_s.split(':').map(&:to_i) max_t ||= min_t mb = (@max_body_size.to_f / 1_048_576).round(1) # Use Puma as the application server for concurrency. @app_class.set :server, :puma @app_class.set :server_settings, { workers: w, min_threads: min_t, max_threads: max_t } # Background thread to purge stale rate-limit entries. if @rate_limiter rl = @rate_limiter Thread.new do loop do sleep rl.window_seconds * 2 rl.cleanup! end end end rl_info = if @rate_limiter "#{@rate_limiter.max_requests} req / #{@rate_limiter.window_seconds}s per IP" else 'Disabled' end auth_info = [] auth_info << "Bearer Token" if @token auth_info << "JWT (HS256)" if @jwt_secret auth_display = auth_info.empty? ? "Public (no token)" : auth_info.join(' + ') puts "=============================================" puts " GR API MANAGER STARTED" puts " Port : #{@port}" puts " Auth : #{auth_display}" puts " Prefix : #{@prefix.empty? ? '/' : @prefix}" puts " Max Body : #{mb} MB" puts " Workers : #{w} | Threads: #{min_t}:#{max_t}" puts " Rate Limit: #{rl_info}" puts " Dev Mode : #{@dev_mode ? 'ON ⚠️ (disable in production)' : 'Off'}" puts "=============================================" @app_class.run! end |