Skip to main content

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", &params, 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", &params, 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}