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 +7 -0
- data/README.md +74 -0
- data/infraforge.gemspec +21 -0
- data/lib/infraforge/version.rb +5 -0
- data/lib/infraforge.rb +205 -0
- metadata +45 -0
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
|
data/infraforge.gemspec
ADDED
|
@@ -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
|
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: []
|