nautilus_rs/resources/gate/mod.rs
1pub mod types;
2
3use std::sync::Arc;
4
5use crate::{error::Error, http::HttpClient};
6use types::{
7 AccessToken, AuthorizationDecision, AuthorizeParams, CreateIdentityParams, CreateTokenParams,
8 Identity, JsonPatchOp, OidcProvider, SecuritySettings, TokenInfo,
9};
10
11/// Gate service client — Auth-as-a-Service.
12///
13/// Provides identity management, short-lived access token issuance, and
14/// policy-based authorization checks.
15///
16/// Obtain a `Gate` instance either as part of the unified [`Verne`] client or
17/// standalone:
18///
19/// ```no_run
20/// // Standalone
21/// use nautilus_rs::Gate;
22/// let gate = Gate::new("vrn_gate_live_sk_…");
23///
24/// // Via unified client
25/// use nautilus_rs::Verne;
26/// # fn run() -> Result<(), nautilus_rs::Error> {
27/// let verne = Verne::builder().gate("vrn_gate_live_sk_…").build()?;
28/// let gate = verne.gate()?;
29/// # Ok(())
30/// # }
31/// ```
32///
33/// [`Verne`]: crate::Verne
34pub struct Gate {
35 http: Arc<HttpClient>,
36 api_key: String,
37}
38
39impl std::fmt::Debug for Gate {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("Gate").finish_non_exhaustive()
42 }
43}
44
45impl Gate {
46 /// Create a `Gate` client with default settings.
47 ///
48 /// Panics if the API key is empty or the HTTP client cannot be initialized.
49 /// Use [`Gate::builder`] for fallible construction.
50 pub fn new(api_key: impl Into<String>) -> Self {
51 let key = api_key.into();
52 Self::builder()
53 .api_key(&key)
54 .build()
55 .expect("failed to build Gate client")
56 }
57
58 /// Return a [`GateBuilder`] for fine-grained configuration.
59 pub fn builder() -> GateBuilder {
60 GateBuilder::default()
61 }
62
63 pub(crate) fn from_http(http: Arc<HttpClient>, api_key: String) -> Self {
64 Self { http, api_key }
65 }
66
67 /// Return an [`IdentitiesClient`] for CRUD operations on identities.
68 pub fn identities(&self) -> IdentitiesClient {
69 IdentitiesClient {
70 http: Arc::clone(&self.http),
71 }
72 }
73
74 /// Return a [`TokensClient`] for issuing and introspecting access tokens.
75 pub fn tokens(&self) -> TokensClient {
76 TokensClient {
77 http: Arc::clone(&self.http),
78 api_key: self.api_key.clone(),
79 }
80 }
81
82 /// Return a [`SettingsClient`] for reading and updating tenant settings.
83 pub fn settings(&self) -> SettingsClient {
84 SettingsClient {
85 http: Arc::clone(&self.http),
86 }
87 }
88
89 /// Check whether a subject is allowed to perform an action on a resource.
90 ///
91 /// Maps to `POST /v1/gate/authorize`.
92 ///
93 /// # Example
94 ///
95 /// ```no_run
96 /// use nautilus_rs::{Gate, AuthorizeParams};
97 ///
98 /// # async fn run() -> Result<(), nautilus_rs::Error> {
99 /// let gate = Gate::new("vrn_gate_live_sk_…");
100 /// let decision = gate.authorize(AuthorizeParams {
101 /// subject: "idn_alice".into(),
102 /// action: "read".into(),
103 /// resource: "report:rpt_456".into(),
104 /// context: None,
105 /// }).await?;
106 /// assert!(decision.allowed);
107 /// # Ok(())
108 /// # }
109 /// ```
110 pub async fn authorize(&self, params: AuthorizeParams) -> Result<AuthorizationDecision, Error> {
111 self.http.post("/v1/gate/authorize", ¶ms, false).await
112 }
113
114 /// List the social login providers currently enabled for a tenant.
115 ///
116 /// This is a **public, unauthenticated** endpoint — call it from your
117 /// login / registration page to decide which social buttons to render.
118 /// Maps to `GET /public/gate/providers/{tenant_id}`.
119 ///
120 /// # Example
121 ///
122 /// ```no_run
123 /// use nautilus_rs::Gate;
124 ///
125 /// # async fn run() -> Result<(), nautilus_rs::Error> {
126 /// let gate = Gate::new("vrn_gate_live_sk_…");
127 /// let providers = gate.get_enabled_providers("ten_001").await?;
128 /// // → ["github", "google"]
129 /// # Ok(())
130 /// # }
131 /// ```
132 pub async fn get_enabled_providers(&self, tenant_id: &str) -> Result<Vec<String>, Error> {
133 #[derive(serde::Deserialize)]
134 struct Wrapper {
135 providers: Vec<String>,
136 }
137
138 let wrapped: Wrapper = self
139 .http
140 .get(&format!("/public/gate/providers/{tenant_id}"))
141 .await?;
142 Ok(wrapped.providers)
143 }
144
145 /// Initialize a Kratos login flow using your Gate API key.
146 ///
147 /// Call this from your server and pass the returned flow JSON to your
148 /// browser-side code to render social login buttons. The flow already
149 /// contains only the providers your tenant has enabled. Maps to
150 /// `GET /v1/gate/auth/login`.
151 ///
152 /// The response mirrors the raw Ory Kratos flow JSON, so it is returned as
153 /// an untyped [`serde_json::Value`].
154 pub async fn create_login_flow(&self) -> Result<serde_json::Value, Error> {
155 self.http.get("/v1/gate/auth/login").await
156 }
157}
158
159/// Builder for a standalone [`Gate`] client.
160///
161/// # Example
162///
163/// ```no_run
164/// use nautilus_rs::Gate;
165///
166/// let gate = Gate::builder()
167/// .api_key("vrn_gate_live_sk_…")
168/// .timeout_secs(15)
169/// .build()
170/// .expect("invalid configuration");
171/// ```
172#[derive(Default)]
173pub struct GateBuilder {
174 api_key: Option<String>,
175 base_url: Option<String>,
176 timeout_secs: Option<u64>,
177}
178
179impl GateBuilder {
180 /// Set the Gate API key (**required**).
181 pub fn api_key(mut self, key: impl Into<String>) -> Self {
182 self.api_key = Some(key.into());
183 self
184 }
185
186 /// Override the API base URL (default: `https://api.vernesoft.com`).
187 pub fn base_url(mut self, url: impl Into<String>) -> Self {
188 self.base_url = Some(url.into());
189 self
190 }
191
192 /// Set the HTTP request timeout in seconds (default: `30`).
193 pub fn timeout_secs(mut self, secs: u64) -> Self {
194 self.timeout_secs = Some(secs);
195 self
196 }
197
198 /// Consume the builder and return a configured [`Gate`].
199 ///
200 /// # Errors
201 ///
202 /// Returns [`Error::Config`] if the API key was not set.
203 pub fn build(self) -> Result<Gate, Error> {
204 let key = self
205 .api_key
206 .ok_or_else(|| Error::Config("gate API key is required".into()))?;
207 let http = HttpClient::new(&key, self.base_url, self.timeout_secs)?;
208 Ok(Gate {
209 http: Arc::new(http),
210 api_key: key,
211 })
212 }
213}
214
215/// Access to the `/v1/gate/identities` endpoints.
216///
217/// Obtain via [`Gate::identities`].
218pub struct IdentitiesClient {
219 http: Arc<HttpClient>,
220}
221
222impl IdentitiesClient {
223 /// Create a new identity.
224 ///
225 /// Maps to `POST /v1/gate/identities`.
226 pub async fn create(&self, params: CreateIdentityParams) -> Result<Identity, Error> {
227 self.http.post("/v1/gate/identities", ¶ms, false).await
228 }
229
230 /// Fetch a single identity by ID.
231 ///
232 /// Maps to `GET /v1/gate/identities/{id}`.
233 pub async fn get(&self, identity_id: &str) -> Result<Identity, Error> {
234 self.http
235 .get(&format!("/v1/gate/identities/{identity_id}"))
236 .await
237 }
238
239 /// Partially update an identity using [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)
240 /// JSON Patch operations.
241 ///
242 /// Maps to `PATCH /v1/gate/identities/{id}`.
243 pub async fn patch(&self, identity_id: &str, ops: Vec<JsonPatchOp>) -> Result<Identity, Error> {
244 self.http
245 .patch(&format!("/v1/gate/identities/{identity_id}"), &ops)
246 .await
247 }
248
249 /// Permanently delete an identity.
250 ///
251 /// Maps to `DELETE /v1/gate/identities/{id}`.
252 pub async fn delete(&self, identity_id: &str) -> Result<(), Error> {
253 self.http
254 .delete(&format!("/v1/gate/identities/{identity_id}"))
255 .await
256 }
257
258 /// Activate or deactivate an identity.
259 ///
260 /// An `"inactive"` identity cannot log in — Kratos rejects its credentials
261 /// automatically — until it is reactivated. The identity is not deleted.
262 /// Fires the `identity.state_changed` webhook event.
263 ///
264 /// Maps to `PATCH /v1/gate/identities/{id}/state`. `state` must be
265 /// `"active"` or `"inactive"`.
266 pub async fn set_state(&self, identity_id: &str, state: &str) -> Result<Identity, Error> {
267 #[derive(serde::Serialize)]
268 struct StateBody<'a> {
269 state: &'a str,
270 }
271
272 self.http
273 .patch(
274 &format!("/v1/gate/identities/{identity_id}/state"),
275 &StateBody { state },
276 )
277 .await
278 }
279
280 /// Activate an identity — convenience wrapper for [`set_state`](Self::set_state)
281 /// with `"active"`.
282 pub async fn activate(&self, identity_id: &str) -> Result<Identity, Error> {
283 self.set_state(identity_id, "active").await
284 }
285
286 /// Deactivate an identity — convenience wrapper for [`set_state`](Self::set_state)
287 /// with `"inactive"`.
288 pub async fn deactivate(&self, identity_id: &str) -> Result<Identity, Error> {
289 self.set_state(identity_id, "inactive").await
290 }
291
292 /// Trigger a new email verification flow for an identity.
293 ///
294 /// Useful when the original verification email expired or was never
295 /// received; the user receives a fresh verification email.
296 ///
297 /// Maps to `POST /v1/gate/identities/{id}/resend-verification`.
298 pub async fn resend_verification(&self, identity_id: &str) -> Result<(), Error> {
299 self.http
300 .post_discard(&format!("/v1/gate/identities/{identity_id}/resend-verification"))
301 .await
302 }
303}
304
305/// Access to the `/v1/gate/tokens` endpoints.
306///
307/// Obtain via [`Gate::tokens`].
308pub struct TokensClient {
309 http: Arc<HttpClient>,
310 api_key: String,
311}
312
313impl TokensClient {
314 /// Issue a short-lived access token for a subject.
315 ///
316 /// Maps to `POST /v1/gate/tokens`. The API key is sent in the request body
317 /// rather than the `Authorization` header.
318 ///
319 /// # Example
320 ///
321 /// ```no_run
322 /// use nautilus_rs::{Gate, CreateTokenParams};
323 ///
324 /// # async fn run() -> Result<(), nautilus_rs::Error> {
325 /// let gate = Gate::new("vrn_gate_live_sk_…");
326 /// let token = gate.tokens().create(CreateTokenParams {
327 /// subject: "idn_alice".into(),
328 /// scopes: Some(vec!["read:profile".into()]),
329 /// ttl_seconds: Some(900),
330 /// }).await?;
331 /// println!("expires at {}", token.expires_at);
332 /// # Ok(())
333 /// # }
334 /// ```
335 pub async fn create(&self, params: CreateTokenParams) -> Result<AccessToken, Error> {
336 #[derive(serde::Serialize)]
337 struct CreateTokenBody {
338 api_key: String,
339 subject: String,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 scopes: Option<Vec<String>>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 ttl_seconds: Option<u64>,
344 }
345
346 let body = CreateTokenBody {
347 api_key: self.api_key.clone(),
348 subject: params.subject,
349 scopes: params.scopes,
350 ttl_seconds: params.ttl_seconds,
351 };
352
353 self.http.post("/v1/gate/tokens", &body, true).await
354 }
355
356 /// Validate an access token and retrieve its claims.
357 ///
358 /// Maps to `POST /v1/gate/tokens/introspect`. Check
359 /// [`TokenInfo::active`] to determine whether the token is still valid.
360 ///
361 /// # Example
362 ///
363 /// ```no_run
364 /// use nautilus_rs::Gate;
365 ///
366 /// # async fn run() -> Result<(), nautilus_rs::Error> {
367 /// let gate = Gate::new("vrn_gate_live_sk_…");
368 /// let info = gate.tokens().introspect("eyJ…").await?;
369 /// if info.active {
370 /// println!("valid token for {}", info.subject);
371 /// }
372 /// # Ok(())
373 /// # }
374 /// ```
375 pub async fn introspect(&self, access_token: &str) -> Result<TokenInfo, Error> {
376 #[derive(serde::Serialize)]
377 struct IntrospectBody<'a> {
378 access_token: &'a str,
379 }
380
381 self.http
382 .post(
383 "/v1/gate/tokens/introspect",
384 &IntrospectBody { access_token },
385 false,
386 )
387 .await
388 }
389}
390
391/// Access to the `/v1/gate/settings` endpoints.
392///
393/// Obtain via [`Gate::settings`].
394pub struct SettingsClient {
395 http: Arc<HttpClient>,
396}
397
398impl SettingsClient {
399 /// Fetch the tenant's security settings (passwordless / MFA).
400 ///
401 /// Maps to `GET /v1/gate/settings/security`.
402 pub async fn get_security(&self) -> Result<SecuritySettings, Error> {
403 self.http.get("/v1/gate/settings/security").await
404 }
405
406 /// Replace the tenant's security settings.
407 ///
408 /// Both fields are always sent — the update is a full replacement, not a
409 /// merge. Maps to `PUT /v1/gate/settings/security`.
410 ///
411 /// # Example
412 ///
413 /// ```no_run
414 /// use nautilus_rs::{Gate, SecuritySettings};
415 ///
416 /// # async fn run() -> Result<(), nautilus_rs::Error> {
417 /// let gate = Gate::new("vrn_gate_live_sk_…");
418 /// gate.settings().update_security(SecuritySettings {
419 /// passwordless_enabled: true,
420 /// mfa_enabled: false,
421 /// }).await?;
422 /// # Ok(())
423 /// # }
424 /// ```
425 pub async fn update_security(&self, settings: SecuritySettings) -> Result<(), Error> {
426 self.http
427 .put_discard("/v1/gate/settings/security", &settings)
428 .await
429 }
430
431 /// List the tenant's social login (OIDC) providers and whether each is
432 /// enabled — covering every provider Gate supports, regardless of state.
433 ///
434 /// Maps to `GET /v1/gate/settings/oidc-providers`.
435 pub async fn get_oidc_providers(&self) -> Result<Vec<OidcProvider>, Error> {
436 #[derive(serde::Deserialize)]
437 struct Wrapper {
438 providers: Vec<OidcProvider>,
439 }
440
441 let wrapped: Wrapper = self.http.get("/v1/gate/settings/oidc-providers").await?;
442 Ok(wrapped.providers)
443 }
444
445 /// Set the `enabled` flag for one or more social login providers.
446 ///
447 /// Any provider omitted from `providers` is left unchanged. Returns the
448 /// full, updated provider list. Maps to
449 /// `PUT /v1/gate/settings/oidc-providers`.
450 ///
451 /// # Example
452 ///
453 /// ```no_run
454 /// use nautilus_rs::{Gate, OidcProvider};
455 ///
456 /// # async fn run() -> Result<(), nautilus_rs::Error> {
457 /// let gate = Gate::new("vrn_gate_live_sk_…");
458 /// let providers = gate.settings().update_oidc_providers(vec![
459 /// OidcProvider { provider: "github".into(), enabled: true },
460 /// OidcProvider { provider: "google".into(), enabled: true },
461 /// ]).await?;
462 /// # Ok(())
463 /// # }
464 /// ```
465 pub async fn update_oidc_providers(
466 &self,
467 providers: Vec<OidcProvider>,
468 ) -> Result<Vec<OidcProvider>, Error> {
469 #[derive(serde::Serialize)]
470 struct Body {
471 providers: Vec<OidcProvider>,
472 }
473
474 #[derive(serde::Deserialize)]
475 struct Wrapper {
476 providers: Vec<OidcProvider>,
477 }
478
479 let wrapped: Wrapper = self
480 .http
481 .put("/v1/gate/settings/oidc-providers", &Body { providers })
482 .await?;
483 Ok(wrapped.providers)
484 }
485}