infraforge 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f74bcdd2929604a5104b7e3b5e55e6624db716dac9cb57a32aff3d6c1aef70e3
4
+ data.tar.gz: eed45a427b81a4ac159a30a87a931066f8410257bbc75c29ea4a829a2afa57d5
5
+ SHA512:
6
+ metadata.gz: 969ab53e6b0ccc705aa9822316afb1b78fe6384138226d387568fb898a6a25fc128b65d47a7deed797b251da06ec588e9a9312553c7eb19c934fdd17bc920c6a
7
+ data.tar.gz: 1c824442fecddac7185cf151dbbccaeee69f8d38457e40e96c58ebce5a29884912a2f65050230b90df6cfeda290119869e679c509a0b42fabf7beb9e17e5d8da
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # InfraForge — Ruby SDK
2
+
3
+ Official Ruby client for [InfraForge](https://infraforge.digitalforge.ia.br).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ gem install infraforge
9
+ ```
10
+
11
+ Or add to `Gemfile`:
12
+
13
+ ```ruby
14
+ gem "infraforge"
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```ruby
20
+ require "infraforge"
21
+
22
+ c = InfraForge::Client.new(
23
+ url: "https://infraforge.example.com",
24
+ project_slug: "my-app",
25
+ api_key: "if_xxxxx",
26
+ )
27
+
28
+ # Sign in (stores session in memory)
29
+ c.sign_in(email: "alice@example.com", password: "secret")
30
+
31
+ # Run a SQL query — RLS enforced via JWT
32
+ rows = c.query("SELECT id, title FROM posts WHERE author_id = current_user_id()")
33
+ rows.each { |r| puts r["title"] }
34
+
35
+ # Parameterized
36
+ rows = c.query("SELECT * FROM posts WHERE id = $1", [42])
37
+
38
+ # Sign up (admin may need to approve)
39
+ res = c.sign_up(email: "alice@example.com", password: "secret", name: "Alice")
40
+ puts "pending approval" if res[:pending]
41
+
42
+ # Reset password
43
+ c.request_password_reset("alice@example.com")
44
+
45
+ # Sign out
46
+ c.sign_out
47
+ ```
48
+
49
+ ## Social login (browser flow)
50
+
51
+ ```ruby
52
+ url = c.social_login_url(provider: "google", redirect_to: "https://app.example.com/callback")
53
+ # Redirect user to `url`. After auth, they land at redirect_to#token=<jwt>.
54
+ # Extract the fragment in JS, POST to your Ruby service, then:
55
+ c.set_token(jwt)
56
+ ```
57
+
58
+ ## Errors
59
+
60
+ ```ruby
61
+ begin
62
+ c.sign_in(email: "a@b.com", password: "wrong")
63
+ rescue InfraForge::AuthError => e
64
+ if e.pending
65
+ puts "pending admin approval"
66
+ else
67
+ puts "login failed: #{e.message}"
68
+ end
69
+ end
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/infraforge/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "infraforge"
7
+ spec.version = InfraForge::VERSION
8
+ spec.authors = ["InfraForge"]
9
+
10
+ spec.summary = "Official Ruby SDK for InfraForge."
11
+ spec.description = "Auth + database query client for the InfraForge self-hosted project provisioning platform."
12
+ spec.homepage = "https://infraforge.digitalforge.ia.br"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/tuliobarreto1/Infraforge"
18
+
19
+ spec.files = Dir["lib/**/*.rb", "README.md", "infraforge.gemspec"]
20
+ spec.require_paths = ["lib"]
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InfraForge
4
+ VERSION = "0.1.0"
5
+ end
data/lib/infraforge.rb ADDED
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require "base64"
7
+
8
+ require_relative "infraforge/version"
9
+
10
+ # InfraForge SDK — auth + database query against a project.
11
+ #
12
+ # Example:
13
+ # c = InfraForge::Client.new(url: "https://infraforge.example.com",
14
+ # project_slug: "my-app",
15
+ # api_key: "if_xxx")
16
+ # c.sign_in(email: "alice@example.com", password: "secret")
17
+ # rows = c.query("SELECT id FROM posts WHERE author_id = current_user_id()")
18
+ module InfraForge
19
+ User = Struct.new(:id, :email, :role, :metadata, :created_at, keyword_init: true)
20
+ Session = Struct.new(:user, :token, keyword_init: true)
21
+
22
+ class AuthError < StandardError
23
+ attr_reader :pending
24
+
25
+ def initialize(message, pending: false)
26
+ super(message)
27
+ @pending = pending
28
+ end
29
+ end
30
+
31
+ class QueryError < StandardError; end
32
+
33
+ class Client
34
+ attr_reader :session
35
+
36
+ def initialize(url:, project_slug:, api_key:, timeout: 30)
37
+ @url = url.chomp("/")
38
+ @project_slug = project_slug
39
+ @api_key = api_key
40
+ @timeout = timeout
41
+ @session = nil
42
+ end
43
+
44
+ def token
45
+ @session&.token
46
+ end
47
+
48
+ # Manually set a JWT (e.g., received from external OAuth callback).
49
+ def set_token(token)
50
+ parts = token.split(".")
51
+ user = User.new(id: "", email: "", role: "authenticated")
52
+ if parts.size == 3
53
+ begin
54
+ payload = JSON.parse(base64url_decode(parts[1]))
55
+ user = User.new(
56
+ id: payload["sub"].to_s,
57
+ email: payload["email"].to_s,
58
+ role: (payload["role"] || "authenticated").to_s,
59
+ )
60
+ rescue StandardError
61
+ # leave default user
62
+ end
63
+ end
64
+ @session = Session.new(user: user, token: token)
65
+ end
66
+
67
+ # Register a new user. Returns { user:, pending:, message: }.
68
+ # If admin approval is required, status is 202 and pending is true.
69
+ def sign_up(email:, password:, name: nil)
70
+ body = { email: email, password: password }
71
+ body[:name] = name unless name.nil?
72
+ status, data = request(:post, auth_url("/register"), body: body)
73
+ raise AuthError, error_msg(data, status) if status >= 400
74
+
75
+ {
76
+ user: data["user"] ? user_from_hash(data["user"]) : nil,
77
+ pending: status == 202,
78
+ message: data["message"],
79
+ }
80
+ end
81
+
82
+ # Sign in. Stores session in memory.
83
+ def sign_in(email:, password:)
84
+ status, data = request(:post, auth_url("/login"), body: { email: email, password: password })
85
+ if status >= 400
86
+ pending = status == 403
87
+ raise AuthError.new(error_msg(data, status), pending: pending)
88
+ end
89
+
90
+ raise AuthError, "Malformed login response" unless data["user"] && data["token"]
91
+
92
+ @session = Session.new(user: user_from_hash(data["user"]), token: data["token"])
93
+ end
94
+
95
+ # Fetch current user from server.
96
+ def get_user
97
+ raise AuthError, "Not authenticated" if @session.nil?
98
+
99
+ status, data = request(:get, auth_url("/me"),
100
+ headers: { "Authorization" => "Bearer #{@session.token}" })
101
+ if status >= 400
102
+ @session = nil if status == 401
103
+ raise AuthError, error_msg(data, status)
104
+ end
105
+
106
+ user = user_from_hash(data.fetch("user"))
107
+ @session = Session.new(user: user, token: @session.token)
108
+ user
109
+ end
110
+
111
+ # Request password reset email (silent, no enumeration).
112
+ def request_password_reset(email)
113
+ status, data = request(:post, auth_url("/forgot-password"), body: { email: email })
114
+ raise AuthError, error_msg(data, status) if status >= 400
115
+
116
+ true
117
+ end
118
+
119
+ # Clear the in-memory session.
120
+ def sign_out
121
+ @session = nil
122
+ end
123
+
124
+ # Build the OAuth start URL for a browser flow.
125
+ def social_login_url(provider:, redirect_to:)
126
+ raise ArgumentError, "provider must be 'google' or 'github'" unless %w[google github].include?(provider)
127
+
128
+ "#{auth_url("/oauth/#{provider}/start")}?redirectTo=#{URI.encode_www_form_component(redirect_to)}"
129
+ end
130
+
131
+ # Run a SQL query — RLS enforced via current JWT.
132
+ def query(sql, params = [])
133
+ raise QueryError, "Not authenticated. Call sign_in first." if @session.nil?
134
+
135
+ status, data = request(:post, "#{@url}/api/query/#{@project_slug}",
136
+ headers: {
137
+ "x-infraforge-key" => @api_key,
138
+ "Authorization" => "Bearer #{@session.token}",
139
+ },
140
+ body: { query: sql, params: params })
141
+ raise QueryError, error_msg(data, status) if status >= 400
142
+
143
+ data["data"] || []
144
+ end
145
+
146
+ private
147
+
148
+ def auth_url(path)
149
+ "#{@url}/api/auth/#{@project_slug}#{path}"
150
+ end
151
+
152
+ def request(method, url, headers: {}, body: nil)
153
+ uri = URI(url)
154
+ http = Net::HTTP.new(uri.host, uri.port)
155
+ http.use_ssl = uri.scheme == "https"
156
+ http.read_timeout = @timeout
157
+ http.open_timeout = @timeout
158
+
159
+ req_class =
160
+ case method
161
+ when :get then Net::HTTP::Get
162
+ when :post then Net::HTTP::Post
163
+ when :put then Net::HTTP::Put
164
+ when :delete then Net::HTTP::Delete
165
+ else raise ArgumentError, "unsupported method: #{method}"
166
+ end
167
+
168
+ req = req_class.new(uri.request_uri)
169
+ headers.each { |k, v| req[k] = v }
170
+ if body
171
+ req["Content-Type"] = "application/json"
172
+ req.body = JSON.generate(body)
173
+ end
174
+
175
+ resp = http.request(req)
176
+ data =
177
+ begin
178
+ JSON.parse(resp.body || "{}")
179
+ rescue JSON::ParserError
180
+ {}
181
+ end
182
+ [resp.code.to_i, data]
183
+ end
184
+
185
+ def error_msg(data, status)
186
+ msg = data["error"]
187
+ msg && !msg.empty? ? msg : "HTTP #{status}"
188
+ end
189
+
190
+ def user_from_hash(h)
191
+ User.new(
192
+ id: h["id"].to_s,
193
+ email: h["email"].to_s,
194
+ role: (h["role"] || "authenticated").to_s,
195
+ metadata: h["metadata"],
196
+ created_at: h["created_at"],
197
+ )
198
+ end
199
+
200
+ def base64url_decode(s)
201
+ pad = (4 - (s.length % 4)) % 4
202
+ Base64.urlsafe_decode64(s + ("=" * pad))
203
+ end
204
+ end
205
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: infraforge
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - InfraForge
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Auth + database query client for the InfraForge self-hosted project provisioning
13
+ platform.
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - README.md
19
+ - infraforge.gemspec
20
+ - lib/infraforge.rb
21
+ - lib/infraforge/version.rb
22
+ homepage: https://infraforge.digitalforge.ia.br
23
+ licenses:
24
+ - MIT
25
+ metadata:
26
+ homepage_uri: https://infraforge.digitalforge.ia.br
27
+ source_code_uri: https://github.com/tuliobarreto1/Infraforge
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '3.0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubygems_version: 4.0.6
43
+ specification_version: 4
44
+ summary: Official Ruby SDK for InfraForge.
45
+ test_files: []