supabase-rb 2.0.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/lib/supabase/README.md +90 -0
- data/lib/supabase/auth/README.md +172 -0
- data/lib/supabase/auth/admin_api.rb +218 -0
- data/lib/supabase/auth/admin_oauth_api.rb +51 -0
- data/lib/supabase/auth/api.rb +125 -0
- data/lib/supabase/auth/async/admin_api.rb +36 -0
- data/lib/supabase/auth/async/admin_oauth_api.rb +15 -0
- data/lib/supabase/auth/async/api.rb +32 -0
- data/lib/supabase/auth/async/client.rb +33 -0
- data/lib/supabase/auth/async.rb +14 -0
- data/lib/supabase/auth/client.rb +1217 -0
- data/lib/supabase/auth/constants.rb +32 -0
- data/lib/supabase/auth/errors.rb +207 -0
- data/lib/supabase/auth/helpers.rb +222 -0
- data/lib/supabase/auth/memory_storage.rb +25 -0
- data/lib/supabase/auth/storage.rb +19 -0
- data/lib/supabase/auth/timer.rb +40 -0
- data/lib/supabase/auth/types.rb +517 -0
- data/lib/supabase/auth/version.rb +7 -0
- data/lib/supabase/auth.rb +19 -0
- data/lib/supabase/client.rb +200 -0
- data/lib/supabase/client_options.rb +82 -0
- data/lib/supabase/functions/README.md +71 -0
- data/lib/supabase/functions/async/client.rb +45 -0
- data/lib/supabase/functions/async.rb +8 -0
- data/lib/supabase/functions/client.rb +174 -0
- data/lib/supabase/functions/errors.rb +38 -0
- data/lib/supabase/functions/types.rb +37 -0
- data/lib/supabase/functions/version.rb +7 -0
- data/lib/supabase/functions.rb +11 -0
- data/lib/supabase/postgrest/README.md +84 -0
- data/lib/supabase/postgrest/async/client.rb +50 -0
- data/lib/supabase/postgrest/async.rb +8 -0
- data/lib/supabase/postgrest/client.rb +136 -0
- data/lib/supabase/postgrest/errors.rb +49 -0
- data/lib/supabase/postgrest/request_builder.rb +657 -0
- data/lib/supabase/postgrest/types.rb +60 -0
- data/lib/supabase/postgrest/utils.rb +24 -0
- data/lib/supabase/postgrest/version.rb +7 -0
- data/lib/supabase/postgrest.rb +13 -0
- data/lib/supabase/realtime/README.md +90 -0
- data/lib/supabase/realtime/channel.rb +274 -0
- data/lib/supabase/realtime/client.rb +182 -0
- data/lib/supabase/realtime/errors.rb +19 -0
- data/lib/supabase/realtime/message.rb +38 -0
- data/lib/supabase/realtime/presence.rb +136 -0
- data/lib/supabase/realtime/push.rb +48 -0
- data/lib/supabase/realtime/socket.rb +40 -0
- data/lib/supabase/realtime/sockets/async_websocket.rb +175 -0
- data/lib/supabase/realtime/sockets/websocket_client_simple.rb +94 -0
- data/lib/supabase/realtime/test_socket.rb +65 -0
- data/lib/supabase/realtime/transformers.rb +26 -0
- data/lib/supabase/realtime/types.rb +70 -0
- data/lib/supabase/realtime/version.rb +7 -0
- data/lib/supabase/realtime.rb +18 -0
- data/lib/supabase/storage/README.md +108 -0
- data/lib/supabase/storage/analytics.rb +69 -0
- data/lib/supabase/storage/async/client.rb +52 -0
- data/lib/supabase/storage/async.rb +8 -0
- data/lib/supabase/storage/bucket_api.rb +65 -0
- data/lib/supabase/storage/client.rb +80 -0
- data/lib/supabase/storage/errors.rb +32 -0
- data/lib/supabase/storage/file_api.rb +281 -0
- data/lib/supabase/storage/request.rb +63 -0
- data/lib/supabase/storage/types.rb +236 -0
- data/lib/supabase/storage/utils.rb +35 -0
- data/lib/supabase/storage/vectors.rb +189 -0
- data/lib/supabase/storage/version.rb +7 -0
- data/lib/supabase/storage.rb +17 -0
- data/lib/supabase/version.rb +5 -0
- data/lib/supabase-auth.rb +3 -0
- data/lib/supabase.rb +63 -0
- metadata +272 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1640366f383a39fbba5df993fe27d204981f99672377fc6982403b4259f6cd81
|
|
4
|
+
data.tar.gz: d332d7b5a8c3bd0f698263988a8c949c132dbbe51908007b7bf5321d8e53a22b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 11f501920c972f3dde5fee01049df4f95fd2aaff1ff6bc2e29c093e8f4c61671900e9caab52e9b870fa86a430a05d0cdbfaf5c4e957e418a389397cfd4ec941f
|
|
7
|
+
data.tar.gz: 582991118f05d8171764efa5788d97234c013f8f7eacc84eefe903959068f20a7d083eaf09ead84a4dc76d225a318c6d69e70b96654f97e0254d184a13bbec39
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# `supabase-rb`
|
|
2
|
+
|
|
3
|
+
Ruby client for [Supabase](https://supabase.com). Umbrella gem that exposes
|
|
4
|
+
Auth, PostgREST, Storage, Edge Functions, and Realtime through a single
|
|
5
|
+
`Supabase.create_client` factory.
|
|
6
|
+
|
|
7
|
+
- Documentation: [supabase.com/docs](https://supabase.com/docs/reference)
|
|
8
|
+
- Source: [github.com/supabase-ruby/supabase-rb](https://github.com/supabase-ruby/supabase-rb)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "supabase-rb"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then `bundle install`. (Requires Ruby >= 3.0.) The Ruby require path is
|
|
17
|
+
`require "supabase"` — only the gem name differs.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "supabase"
|
|
23
|
+
|
|
24
|
+
client = Supabase.create_client(
|
|
25
|
+
supabase_url: ENV["SUPABASE_URL"],
|
|
26
|
+
supabase_key: ENV["SUPABASE_ANON_KEY"]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Auth
|
|
30
|
+
client.auth.sign_in_with_password(email: "user@example.com", password: "pw")
|
|
31
|
+
|
|
32
|
+
# PostgREST
|
|
33
|
+
users = client.from("users").select("id, name").eq("status", "active").execute
|
|
34
|
+
|
|
35
|
+
# Storage
|
|
36
|
+
client.storage.from("avatars").upload("user1.png", File.binread("user1.png"),
|
|
37
|
+
content_type: "image/png")
|
|
38
|
+
|
|
39
|
+
# Edge Functions
|
|
40
|
+
result = client.functions.invoke("hello-world", body: { name: "Ada" })
|
|
41
|
+
|
|
42
|
+
# Realtime (you bring the WebSocket transport — see supabase-realtime)
|
|
43
|
+
channel = client.realtime.channel("realtime:public:users")
|
|
44
|
+
channel.on_postgres_changes("INSERT", schema: "public", table: "users") { |p| puts p }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pass `async: true` to swap Auth / PostgREST / Storage / Functions into their
|
|
48
|
+
async variants (Realtime stays sync):
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require "async"
|
|
52
|
+
|
|
53
|
+
async_client = Supabase.create_client(
|
|
54
|
+
supabase_url: ENV["SUPABASE_URL"],
|
|
55
|
+
supabase_key: ENV["SUPABASE_ANON_KEY"],
|
|
56
|
+
async: true
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
Async do |task|
|
|
60
|
+
jobs = user_ids.map do |id|
|
|
61
|
+
task.async { async_client.from("users").select("*").eq("id", id).execute }
|
|
62
|
+
end
|
|
63
|
+
jobs.map(&:wait)
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`client.set_auth(jwt)` rotates the `Authorization` header across every
|
|
68
|
+
sub-client at once — useful after `auth.sign_in` returns a fresh user JWT.
|
|
69
|
+
|
|
70
|
+
## URL routing
|
|
71
|
+
|
|
72
|
+
Sub-client URLs are derived from the project URL:
|
|
73
|
+
|
|
74
|
+
| Sub-client | URL |
|
|
75
|
+
|---|---|
|
|
76
|
+
| Auth | `<project>/auth/v1` |
|
|
77
|
+
| PostgREST | `<project>/rest/v1` |
|
|
78
|
+
| Storage | `<project>/storage/v1` |
|
|
79
|
+
| Functions | `<project>/functions/v1` |
|
|
80
|
+
| Realtime | `wss://<host>/realtime/v1/websocket` |
|
|
81
|
+
|
|
82
|
+
## Modules
|
|
83
|
+
|
|
84
|
+
`supabase-rb` packages every module in one gem. Per-module references:
|
|
85
|
+
|
|
86
|
+
- [Auth](auth/README.md)
|
|
87
|
+
- [PostgREST](postgrest/README.md)
|
|
88
|
+
- [Storage](storage/README.md)
|
|
89
|
+
- [Edge Functions](functions/README.md)
|
|
90
|
+
- [Realtime](realtime/README.md)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# `supabase-auth`
|
|
2
|
+
|
|
3
|
+
Ruby client for [Supabase Auth](https://supabase.com/docs/guides/auth)
|
|
4
|
+
(GoTrue). Sign-in flows, session management, MFA, JWT verification, OAuth,
|
|
5
|
+
and admin user management. Mirrors the public surface of
|
|
6
|
+
[`supabase_auth`](https://github.com/supabase/supabase-py/tree/main/src/auth)
|
|
7
|
+
in Python.
|
|
8
|
+
|
|
9
|
+
- Source: [github.com/supabase-rb/client](https://github.com/supabase-rb/client)
|
|
10
|
+
- RubyGems: [rubygems.org/gems/supabase-auth](https://rubygems.org/gems/supabase-auth)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "supabase-auth"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then `bundle install`. (Requires Ruby >= 3.0.)
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "supabase/auth"
|
|
24
|
+
|
|
25
|
+
client = Supabase::Auth::Client.new(
|
|
26
|
+
url: "https://your-project.supabase.co/auth/v1",
|
|
27
|
+
headers: { "apiKey" => "your-anon-key" }
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
response = client.sign_in_with_password(email: "user@example.com", password: "pw")
|
|
31
|
+
session = client.get_session
|
|
32
|
+
user = client.get_user
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Sign-in methods
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
client.sign_in_with_password(email:, password:)
|
|
39
|
+
client.sign_in_with_otp(email:) # magic link
|
|
40
|
+
client.sign_in_with_otp(phone:) # SMS OTP
|
|
41
|
+
client.sign_in_with_oauth(provider: "google")
|
|
42
|
+
client.sign_in_with_sso(domain: "company.com")
|
|
43
|
+
client.sign_in_with_id_token(provider:, token:)
|
|
44
|
+
client.sign_in_anonymously
|
|
45
|
+
client.sign_out
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Session lifecycle
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
client.set_session("access_token", "refresh_token")
|
|
52
|
+
client.refresh_session
|
|
53
|
+
client.exchange_code_for_session(auth_code: "code") # PKCE
|
|
54
|
+
|
|
55
|
+
subscription = client.on_auth_state_change { |event, session| ... }
|
|
56
|
+
subscription.unsubscribe.call
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Events: `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`, `USER_UPDATED`,
|
|
60
|
+
`MFA_CHALLENGE_VERIFIED`, `PASSWORD_RECOVERY`.
|
|
61
|
+
|
|
62
|
+
### MFA
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
enrolled = client.mfa.enroll(factor_type: "totp")
|
|
66
|
+
challenge = client.mfa.challenge(factor_id: enrolled["id"])
|
|
67
|
+
client.mfa.verify(factor_id: enrolled["id"], challenge_id: challenge.id, code: "123456")
|
|
68
|
+
|
|
69
|
+
client.mfa.challenge_and_verify(factor_id: enrolled["id"], code: "123456")
|
|
70
|
+
client.mfa.get_authenticator_assurance_level
|
|
71
|
+
client.mfa.list_factors
|
|
72
|
+
client.mfa.unenroll(factor_id: enrolled["id"])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### JWT verification
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
claims = client.get_claims(jwt: "eyJhbG...")
|
|
79
|
+
claims.claims # decoded payload
|
|
80
|
+
claims.headers # JWT headers
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Supports HS256, RS256, ES256, PS256+ (and their 384/512 variants).
|
|
84
|
+
|
|
85
|
+
### Admin API
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
admin = Supabase::Auth::AdminApi.new(
|
|
89
|
+
url: "https://your-project.supabase.co/auth/v1",
|
|
90
|
+
headers: { "Authorization" => "Bearer #{service_role}", "apiKey" => service_role }
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
admin.create_user(email:, password:)
|
|
94
|
+
admin.list_users(page: 1, per_page: 50)
|
|
95
|
+
admin.invite_user_by_email("user@example.com")
|
|
96
|
+
admin.generate_link(type: "signup", email:, password:)
|
|
97
|
+
|
|
98
|
+
# OAuth 2.1 client administration (when the OAuth server feature is enabled)
|
|
99
|
+
admin.oauth.create_client(client_name:, redirect_uris:)
|
|
100
|
+
admin.oauth.list_clients(page: 1, per_page: 20)
|
|
101
|
+
admin.oauth.regenerate_client_secret("client-uuid")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Async variant
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
require "supabase/auth/async"
|
|
108
|
+
|
|
109
|
+
async_client = Supabase::Auth::Async::Client.new(
|
|
110
|
+
url: "https://your-project.supabase.co/auth/v1",
|
|
111
|
+
headers: { "apiKey" => "your-anon-key" }
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
Async do
|
|
115
|
+
user = async_client.get_user
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Built on [`async-http-faraday`](https://github.com/socketry/async-http-faraday).
|
|
120
|
+
Loaded only when you `require "supabase/auth/async"` so sync-only users pay
|
|
121
|
+
zero cost.
|
|
122
|
+
|
|
123
|
+
### Constructor options
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Supabase::Auth::Client.new(
|
|
127
|
+
url: "...",
|
|
128
|
+
headers: { "apiKey" => "..." },
|
|
129
|
+
auto_refresh_token: true,
|
|
130
|
+
persist_session: true,
|
|
131
|
+
detect_session_in_url: true,
|
|
132
|
+
flow_type: "implicit", # or "pkce"
|
|
133
|
+
storage: custom_storage
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Ruby-specific additions
|
|
138
|
+
|
|
139
|
+
The Ruby port carries two intentional enhancements over `supabase_auth` (py),
|
|
140
|
+
documented here so they don't get "fixed" to match Python.
|
|
141
|
+
|
|
142
|
+
### `AuthPKCEError`
|
|
143
|
+
|
|
144
|
+
`Supabase::Auth::Errors::AuthPKCEError` is a dedicated exception for
|
|
145
|
+
PKCE-flow failures (missing or invalid `code_verifier` during
|
|
146
|
+
`exchange_code_for_session`). Python raises a generic `AuthError`; the
|
|
147
|
+
dedicated class gives callers a precise `rescue` target.
|
|
148
|
+
|
|
149
|
+
### Explicit JWT algorithm → digest mapping
|
|
150
|
+
|
|
151
|
+
`Supabase::Auth::Client::ALG_TO_DIGEST` is a frozen lookup table:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
ALG_TO_DIGEST = {
|
|
155
|
+
"RS256" => "SHA256", "RS384" => "SHA384", "RS512" => "SHA512",
|
|
156
|
+
"ES256" => "SHA256", "ES384" => "SHA384", "ES512" => "SHA512",
|
|
157
|
+
"PS256" => "SHA256", "PS384" => "SHA384", "PS512" => "SHA512"
|
|
158
|
+
}.freeze
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Python resolves algorithms dynamically via `PyJWT.get_algorithm_by_name`.
|
|
162
|
+
The Ruby table makes the supported set readable in one place and fails fast
|
|
163
|
+
(`AuthInvalidJwtError`) on unsupported `alg` values.
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
Integration tests need the GoTrue stack on ports 9996–9999:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
docker compose -f infra/docker-compose.yml up -d
|
|
171
|
+
bundle exec rspec spec/supabase/auth/ spec/client_spec.rb spec/admin_api_spec.rb
|
|
172
|
+
```
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Supabase
|
|
6
|
+
module Auth
|
|
7
|
+
# Admin API for managing users with a service role key.
|
|
8
|
+
# Provides CRUD operations on users, link generation, and MFA management.
|
|
9
|
+
class AdminApi < Api
|
|
10
|
+
# @return [AdminOAuthApi] OAuth 2.1 client administration accessor
|
|
11
|
+
attr_reader :oauth
|
|
12
|
+
|
|
13
|
+
# @param url [String] The GoTrue API base URL
|
|
14
|
+
# @param headers [Hash] Headers including Authorization bearer token
|
|
15
|
+
# @param http_client [Faraday::Connection, nil] Optional custom Faraday client
|
|
16
|
+
# @param verify [Boolean] Verify TLS certificates (default true)
|
|
17
|
+
# @param proxy [String, nil] HTTP proxy URL
|
|
18
|
+
# @param timeout [Numeric, nil] Per-request timeout in seconds
|
|
19
|
+
def initialize(url:, headers: {}, http_client: nil, verify: true, proxy: nil, timeout: nil)
|
|
20
|
+
super(url: url, headers: headers, http_client: http_client, verify: verify, proxy: proxy, timeout: timeout)
|
|
21
|
+
@oauth = AdminOAuthApi.new(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Creates a new user via the admin API.
|
|
25
|
+
# @param attributes [Hash] user attributes (email, password, user_metadata, app_metadata, etc.)
|
|
26
|
+
# @return [Types::UserResponse]
|
|
27
|
+
def create_user(attributes)
|
|
28
|
+
data = post("admin/users", body: attributes)
|
|
29
|
+
Helpers.parse_user_response(data)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Lists all users.
|
|
33
|
+
# @param page [Integer, nil] page number
|
|
34
|
+
# @param per_page [Integer, nil] users per page
|
|
35
|
+
# @return [Array<Types::User>]
|
|
36
|
+
def list_users(page: nil, per_page: nil)
|
|
37
|
+
params = {}
|
|
38
|
+
params[:page] = page if page
|
|
39
|
+
params[:per_page] = per_page if per_page
|
|
40
|
+
data = get("admin/users", params: params)
|
|
41
|
+
users = data["users"] || []
|
|
42
|
+
users.map { |u| Types::User.from_hash(u) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Gets a user by their ID.
|
|
46
|
+
# @param uid [String] user UUID
|
|
47
|
+
# @return [Types::UserResponse]
|
|
48
|
+
# @raise [ArgumentError] if uid is not a valid UUID
|
|
49
|
+
def get_user_by_id(uid)
|
|
50
|
+
_validate_uuid(uid)
|
|
51
|
+
data = get("admin/users/#{uid}")
|
|
52
|
+
Helpers.parse_user_response(data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Updates a user by their ID.
|
|
56
|
+
# @param uid [String] user UUID
|
|
57
|
+
# @param attributes [Hash] attributes to update
|
|
58
|
+
# @return [Types::UserResponse]
|
|
59
|
+
# @raise [ArgumentError] if uid is not a valid UUID
|
|
60
|
+
def update_user_by_id(uid, attributes)
|
|
61
|
+
_validate_uuid(uid)
|
|
62
|
+
data = put("admin/users/#{uid}", body: attributes)
|
|
63
|
+
Helpers.parse_user_response(data)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Deletes a user by their ID.
|
|
67
|
+
# @param uid [String] user UUID
|
|
68
|
+
# @param should_soft_delete [Boolean] soft delete instead of hard delete
|
|
69
|
+
# @raise [ArgumentError] if uid is not a valid UUID
|
|
70
|
+
def delete_user(uid, should_soft_delete: false)
|
|
71
|
+
_validate_uuid(uid)
|
|
72
|
+
_request("DELETE", "admin/users/#{uid}", body: { should_soft_delete: should_soft_delete })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Generates email links and OTPs.
|
|
76
|
+
def generate_link(params)
|
|
77
|
+
options = params[:options] || params["options"] || {}
|
|
78
|
+
body = {
|
|
79
|
+
type: params[:type] || params["type"],
|
|
80
|
+
email: params[:email] || params["email"],
|
|
81
|
+
password: params[:password] || params["password"],
|
|
82
|
+
new_email: params[:new_email] || params["new_email"],
|
|
83
|
+
data: options[:data] || options["data"]
|
|
84
|
+
}
|
|
85
|
+
redirect_to = options[:redirect_to] || options["redirect_to"]
|
|
86
|
+
query = {}
|
|
87
|
+
query["redirect_to"] = redirect_to if redirect_to
|
|
88
|
+
data = post("admin/generate_link", body: body, params: query)
|
|
89
|
+
Helpers.parse_link_response(data)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Invites a user by email.
|
|
93
|
+
def invite_user_by_email(email, options = {})
|
|
94
|
+
body = { email: email, data: options[:data] || options["data"] }
|
|
95
|
+
redirect_to = options[:redirect_to] || options["redirect_to"]
|
|
96
|
+
query = {}
|
|
97
|
+
query["redirect_to"] = redirect_to if redirect_to
|
|
98
|
+
data = post("invite", body: body, params: query)
|
|
99
|
+
Helpers.parse_user_response(data)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Signs out a user by revoking their session via the admin API.
|
|
103
|
+
def sign_out(access_token, scope = "global")
|
|
104
|
+
_request("POST", "logout", jwt: access_token, params: { "scope" => scope }, no_resolve_json: true)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Lists MFA factors for a user (admin).
|
|
108
|
+
# @param params [Hash] :user_id (required)
|
|
109
|
+
# @return [Types::AuthMFAAdminListFactorsResponse]
|
|
110
|
+
def _list_factors(params)
|
|
111
|
+
user_id = params[:user_id] || params["user_id"]
|
|
112
|
+
_validate_uuid(user_id)
|
|
113
|
+
data = get("admin/users/#{user_id}/factors")
|
|
114
|
+
Types::AuthMFAAdminListFactorsResponse.from_hash(data)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Deletes an MFA factor for a user (admin).
|
|
118
|
+
# @param params [Hash] :user_id and :id (both required)
|
|
119
|
+
# @return [Types::AuthMFAAdminDeleteFactorResponse]
|
|
120
|
+
def _delete_factor(params)
|
|
121
|
+
user_id = params[:user_id] || params["user_id"]
|
|
122
|
+
factor_id = params[:id] || params["id"]
|
|
123
|
+
_validate_uuid(user_id)
|
|
124
|
+
_validate_uuid(factor_id)
|
|
125
|
+
data = delete("admin/users/#{user_id}/factors/#{factor_id}")
|
|
126
|
+
Types::AuthMFAAdminDeleteFactorResponse.from_hash(data)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Lists OAuth clients with optional pagination. Only relevant when the OAuth 2.1
|
|
130
|
+
# server is enabled in Supabase Auth.
|
|
131
|
+
# @param params [Hash, Types::PageParams, nil] optional :page and :per_page
|
|
132
|
+
# @return [Types::OAuthClientListResponse]
|
|
133
|
+
def _list_oauth_clients(params = nil)
|
|
134
|
+
query = {}
|
|
135
|
+
if params
|
|
136
|
+
page = params[:page] || params["page"]
|
|
137
|
+
per_page = params[:per_page] || params["per_page"]
|
|
138
|
+
query[:page] = page if page
|
|
139
|
+
query[:per_page] = per_page if per_page
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
response = _request("GET", "admin/oauth/clients", params: query, no_resolve_json: true)
|
|
143
|
+
body = response.body.is_a?(String) ? JSON.parse(response.body) : (response.body || {})
|
|
144
|
+
result = Types::OAuthClientListResponse.from_hash(body)
|
|
145
|
+
|
|
146
|
+
total = response.headers["x-total-count"] || response.headers["X-Total-Count"]
|
|
147
|
+
result.total = total.to_i if total
|
|
148
|
+
|
|
149
|
+
links = response.headers["link"] || response.headers["Link"]
|
|
150
|
+
if links
|
|
151
|
+
links.split(",").each do |link|
|
|
152
|
+
parts = link.split(";")
|
|
153
|
+
next unless parts.length >= 2
|
|
154
|
+
|
|
155
|
+
page_match = parts[0].split("page=")
|
|
156
|
+
next unless page_match.length >= 2
|
|
157
|
+
|
|
158
|
+
page_num = page_match[1].split("&")[0].sub(/>$/, "").to_i
|
|
159
|
+
rel = parts[1].split("=")[1].to_s.delete('"').strip
|
|
160
|
+
case rel
|
|
161
|
+
when "next" then result.next_page = page_num
|
|
162
|
+
when "last" then result.last_page = page_num
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
result
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Creates a new OAuth client. Only relevant when the OAuth 2.1 server is enabled.
|
|
171
|
+
# @param params [Hash] OAuth client attributes (client_name, redirect_uris, etc.)
|
|
172
|
+
# @return [Types::OAuthClientResponse]
|
|
173
|
+
def _create_oauth_client(params)
|
|
174
|
+
data = post("admin/oauth/clients", body: params)
|
|
175
|
+
Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Gets details of a specific OAuth client.
|
|
179
|
+
# @param client_id [String] OAuth client UUID
|
|
180
|
+
# @return [Types::OAuthClientResponse]
|
|
181
|
+
# @raise [ArgumentError] if client_id is not a valid UUID
|
|
182
|
+
def _get_oauth_client(client_id)
|
|
183
|
+
_validate_uuid(client_id)
|
|
184
|
+
data = get("admin/oauth/clients/#{client_id}")
|
|
185
|
+
Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Updates an OAuth client.
|
|
189
|
+
# @param client_id [String] OAuth client UUID
|
|
190
|
+
# @param params [Hash] attributes to update
|
|
191
|
+
# @return [Types::OAuthClientResponse]
|
|
192
|
+
# @raise [ArgumentError] if client_id is not a valid UUID
|
|
193
|
+
def _update_oauth_client(client_id, params)
|
|
194
|
+
_validate_uuid(client_id)
|
|
195
|
+
data = put("admin/oauth/clients/#{client_id}", body: params)
|
|
196
|
+
Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Deletes an OAuth client.
|
|
200
|
+
# @param client_id [String] OAuth client UUID
|
|
201
|
+
# @raise [ArgumentError] if client_id is not a valid UUID
|
|
202
|
+
def _delete_oauth_client(client_id)
|
|
203
|
+
_validate_uuid(client_id)
|
|
204
|
+
_request("DELETE", "admin/oauth/clients/#{client_id}")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Regenerates the secret for an OAuth client.
|
|
208
|
+
# @param client_id [String] OAuth client UUID
|
|
209
|
+
# @return [Types::OAuthClientResponse]
|
|
210
|
+
# @raise [ArgumentError] if client_id is not a valid UUID
|
|
211
|
+
def _regenerate_oauth_client_secret(client_id)
|
|
212
|
+
_validate_uuid(client_id)
|
|
213
|
+
data = post("admin/oauth/clients/#{client_id}/regenerate_secret")
|
|
214
|
+
Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Supabase
|
|
4
|
+
module Auth
|
|
5
|
+
# OAuth 2.1 client administration. Mirrors supabase-py's SyncGoTrueAdminOAuthAPI.
|
|
6
|
+
# Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
|
|
7
|
+
# Accessed via {AdminApi#oauth}; delegates to the underscored implementations on AdminApi.
|
|
8
|
+
class AdminOAuthApi
|
|
9
|
+
# @param admin [AdminApi]
|
|
10
|
+
def initialize(admin)
|
|
11
|
+
@admin = admin
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param params [Hash, Types::PageParams, nil] optional :page / :per_page
|
|
15
|
+
# @return [Types::OAuthClientListResponse]
|
|
16
|
+
def list_clients(params = nil)
|
|
17
|
+
@admin._list_oauth_clients(params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param params [Hash] new client attributes (client_name, redirect_uris, etc.)
|
|
21
|
+
# @return [Types::OAuthClientResponse]
|
|
22
|
+
def create_client(params)
|
|
23
|
+
@admin._create_oauth_client(params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param client_id [String] OAuth client UUID
|
|
27
|
+
# @return [Types::OAuthClientResponse]
|
|
28
|
+
def get_client(client_id)
|
|
29
|
+
@admin._get_oauth_client(client_id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param client_id [String] OAuth client UUID
|
|
33
|
+
# @param params [Hash] attributes to update
|
|
34
|
+
# @return [Types::OAuthClientResponse]
|
|
35
|
+
def update_client(client_id, params)
|
|
36
|
+
@admin._update_oauth_client(client_id, params)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param client_id [String] OAuth client UUID
|
|
40
|
+
def delete_client(client_id)
|
|
41
|
+
@admin._delete_oauth_client(client_id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param client_id [String] OAuth client UUID
|
|
45
|
+
# @return [Types::OAuthClientResponse] response with rotated client_secret
|
|
46
|
+
def regenerate_client_secret(client_id)
|
|
47
|
+
@admin._regenerate_oauth_client_secret(client_id)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Supabase
|
|
7
|
+
module Auth
|
|
8
|
+
class Api
|
|
9
|
+
CONTENT_TYPE = "application/json;charset=UTF-8"
|
|
10
|
+
UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
11
|
+
|
|
12
|
+
attr_reader :url, :headers
|
|
13
|
+
|
|
14
|
+
# @param url [String] The GoTrue API base URL
|
|
15
|
+
# @param headers [Hash] Default headers to include on every request (e.g., apikey)
|
|
16
|
+
# @param http_client [Faraday::Connection, nil] Optional custom Faraday client
|
|
17
|
+
# @param verify [Boolean] Verify TLS certificates (default true)
|
|
18
|
+
# @param proxy [String, nil] HTTP proxy URL
|
|
19
|
+
# @param timeout [Numeric, nil] Per-request timeout in seconds
|
|
20
|
+
def initialize(url:, headers: {}, http_client: nil, verify: true, proxy: nil, timeout: nil)
|
|
21
|
+
@url = url
|
|
22
|
+
@headers = headers
|
|
23
|
+
@http_client = http_client
|
|
24
|
+
@verify = verify
|
|
25
|
+
@proxy = proxy
|
|
26
|
+
@timeout = timeout
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Central HTTP dispatch method. Builds URL, merges headers (including API version
|
|
30
|
+
# and Authorization), handles redirect_to as query param, parses JSON, applies
|
|
31
|
+
# optional transform, and maps errors via Helpers.handle_exception.
|
|
32
|
+
#
|
|
33
|
+
# @param method [String, Symbol] HTTP method (GET, POST, PUT, DELETE)
|
|
34
|
+
# @param path [String] Request path (relative to base URL)
|
|
35
|
+
# @param jwt [String, nil] Bearer token for Authorization header
|
|
36
|
+
# @param body [Hash, nil] Request body (serialized to JSON)
|
|
37
|
+
# @param params [Hash] Query parameters
|
|
38
|
+
# @param headers [Hash] Additional headers for this request
|
|
39
|
+
# @param redirect_to [String, nil] If present, added as redirect_to query param
|
|
40
|
+
# @param xform [Proc, nil] Optional transform function applied to parsed response
|
|
41
|
+
# @param no_resolve_json [Boolean] If true, return raw Faraday::Response
|
|
42
|
+
# @return [Hash, Object] Parsed JSON response, transformed result, or raw response
|
|
43
|
+
def _request(method, path, jwt: nil, body: nil, params: {}, headers: {}, redirect_to: nil, xform: nil, no_resolve_json: false)
|
|
44
|
+
merged_headers = @headers.merge(headers)
|
|
45
|
+
merged_headers["Content-Type"] ||= CONTENT_TYPE
|
|
46
|
+
merged_headers[Constants::API_VERSION_HEADER_NAME] ||= Constants::API_VERSIONS.keys.last
|
|
47
|
+
merged_headers["Authorization"] = "Bearer #{jwt}" if jwt
|
|
48
|
+
|
|
49
|
+
query = params.dup
|
|
50
|
+
query["redirect_to"] = redirect_to if redirect_to
|
|
51
|
+
|
|
52
|
+
full_path = build_path(path)
|
|
53
|
+
json_body = body ? JSON.generate(body) : nil
|
|
54
|
+
|
|
55
|
+
response = connection.run_request(method.to_s.downcase.to_sym, full_path, json_body, merged_headers) do |req|
|
|
56
|
+
req.params.update(query) unless query.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
result = no_resolve_json ? response : parse_response(response)
|
|
60
|
+
|
|
61
|
+
xform ? xform.call(result) : result
|
|
62
|
+
rescue Faraday::Error => e
|
|
63
|
+
raise Helpers.handle_exception(e)
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
raise Helpers.handle_exception(e)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param id [String] UUID to validate
|
|
69
|
+
# @raise [ArgumentError] if not a valid UUID format
|
|
70
|
+
def _validate_uuid(id)
|
|
71
|
+
unless id.is_a?(String) && id.match?(UUID_REGEX)
|
|
72
|
+
raise ArgumentError, "Invalid id, '#{id}' is not a valid uuid"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Convenience methods that delegate to _request
|
|
77
|
+
|
|
78
|
+
def get(path, headers: {}, params: {})
|
|
79
|
+
_request(:get, path, headers: headers, params: params)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def post(path, body: {}, headers: {}, params: {})
|
|
83
|
+
_request(:post, path, body: body, headers: headers, params: params)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def put(path, body: {}, headers: {}, params: {})
|
|
87
|
+
_request(:put, path, body: body, headers: headers, params: params)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def delete(path, headers: {}, params: {})
|
|
91
|
+
_request(:delete, path, headers: headers, params: params)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def connection
|
|
97
|
+
@connection ||= @http_client || build_connection
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_connection
|
|
101
|
+
Faraday.new(url: @url, ssl: { verify: @verify }, proxy: @proxy) do |f|
|
|
102
|
+
f.response :raise_error
|
|
103
|
+
if @timeout
|
|
104
|
+
f.options.timeout = @timeout
|
|
105
|
+
f.options.open_timeout = @timeout
|
|
106
|
+
end
|
|
107
|
+
f.adapter Faraday.default_adapter
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_path(path)
|
|
112
|
+
base_path = URI.parse(@url).path.chomp("/")
|
|
113
|
+
"#{base_path}/#{path.sub(%r{^/}, '')}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_response(response)
|
|
117
|
+
return {} if response.body.nil? || response.body.empty?
|
|
118
|
+
|
|
119
|
+
JSON.parse(response.body)
|
|
120
|
+
rescue JSON::ParserError
|
|
121
|
+
{}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|