Skip to main content

uarp_sdk/generated/api/
auth.rs

1// Code generated by @uarp/codegen from spec/openapi.json. DO NOT EDIT.
2//!
3//! User registration and OTP authentication
4
5#![allow(unused_imports, clippy::too_many_arguments)]
6
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9
10use crate::client::{Client, Request, NO_BODY, NO_QUERY};
11use crate::error::Result;
12use crate::generated::models;
13use crate::multipart::{field_text, FilePart};
14use crate::util::encode_path;
15
16/// Query and header parameters for `completeOAuthLogin`.
17#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
18pub struct CompleteOAuthLoginParams {
19    pub code: String,
20    pub state: String,
21    /// Provider-side error (e.g. `access_denied`). When present, server returns 400.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub error: Option<String>,
24}
25
26/// Query and header parameters for `startOAuthLogin`.
27#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28pub struct StartOAuthLoginParams {
29    /// Where to redirect after callback. Same-origin paths, Universal-Link hosts (default
30    /// `snaga.ai/auth/callback`), and `snaga://` allowed. Anything else is silently dropped.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub return_to: Option<String>,
33    /// Human-readable device name surfaced on the minted api_key.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub device_label: Option<String>,
36}
37
38/// User registration and OTP authentication
39#[derive(Debug, Clone)]
40pub struct AuthApi {
41    pub(crate) client: Client,
42}
43
44impl Client {
45    /// User registration and OTP authentication
46    pub fn auth(&self) -> AuthApi {
47        AuthApi { client: self.clone() }
48    }
49}
50
51impl AuthApi {
52    /// Sign in with Apple — native iOS handshake
53    ///
54    /// iOS drives the Apple authorization sheet via `ASAuthorizationController` and POSTs the
55    /// resulting `identity_token` (Apple-signed JWT, RS256) here. Server fetches Apple JWKS (cached
56    /// 24h), verifies signature/iss/aud/exp, extracts `sub` + `email`, then mints a session. The
57    /// `email` is only present on first login — subsequent logins reuse the stored
58    /// `apple_sub_to_email` mapping. `is_private_email` (`@privaterelay.appleid.com`) is honoured:
59    /// relay address is stored as the canonical email.
60    ///
61    /// `POST /api/v1/auth/oauth/apple/native`
62    pub async fn apple_native_auth(&self, body: &models::AppleNativeAuthRequest) -> Result<models::AppleNativeAuthResponse> {
63        self.client
64            .request_json(Request {
65                method: Method::POST,
66                path: "/api/v1/auth/oauth/apple/native".to_string(),
67                query: NO_QUERY,
68                body: Some(body),
69                headers: Vec::new(),
70                idempotent: true,
71            })
72            .await
73    }
74
75    /// OAuth callback — exchange code, mint session
76    ///
77    /// Provider redirects here. Server validates state (one-shot, 10 min TTL, provider-bound),
78    /// exchanges `code` for tokens, fetches verified email + IdP `sub`, then mints a session via
79    /// the shared post-verify path. On success: 302 to `${return_to}#api_key=...&email=...` if a
80    /// return_to was stored, else JSON `{api_key, email}`. Refuses to mint for super-admin email
81    /// (Guideline-style separation: super-admin must use OTP).
82    ///
83    /// `GET /api/v1/auth/oauth/{provider}/callback`
84    pub async fn complete_o_auth_login(&self, provider: &models::OAuthLoginProviderConfigStatusProvider, params: &CompleteOAuthLoginParams) -> Result<models::CompleteOAuthLoginResponse> {
85        self.client
86            .request_json(Request {
87                method: Method::GET,
88                path: format!("/api/v1/auth/oauth/{}/callback", encode_path(&provider.to_string())),
89                query: Some(params),
90                body: NO_BODY,
91                headers: Vec::new(),
92                idempotent: false,
93            })
94            .await
95    }
96
97    /// Remove MFA enrolment for the calling user
98    ///
99    /// `POST /api/v1/auth/mfa/disable`
100    pub async fn disable_mfa(&self) -> Result<models::DisableMfaResponse> {
101        self.client
102            .request_json(Request {
103                method: Method::POST,
104                path: "/api/v1/auth/mfa/disable".to_string(),
105                query: NO_QUERY,
106                body: NO_BODY,
107                headers: Vec::new(),
108                idempotent: true,
109            })
110            .await
111    }
112
113    /// Begin MFA enrolment
114    ///
115    /// Returns the TOTP secret (otpauth URL + base32 secret) and one-time recovery codes. Recovery
116    /// codes are shown ONCE and never re-readable; user must store them. Optional body: `{ label?,
117    /// issuer?, algorithm? }`.
118    ///
119    /// `POST /api/v1/auth/mfa/enrol`
120    pub async fn enrol_mfa(&self, body: &models::EnrolMfaRequest) -> Result<models::MfaEnrolment> {
121        self.client
122            .request_json(Request {
123                method: Method::POST,
124                path: "/api/v1/auth/mfa/enrol".to_string(),
125                query: NO_QUERY,
126                body: Some(body),
127                headers: Vec::new(),
128                idempotent: true,
129            })
130            .await
131    }
132
133    /// Trade a mobile hand-off code for the session
134    ///
135    /// Second half of the mobile sign-in hand-off. When `start` is called with
136    /// `app_code_challenge`, the callback redirects to the app with `#code=` instead of
137    /// `#api_key=`, and the key is only released here, to a caller that presents the matching
138    /// verifier. Single-use and short-lived: the code is consumed on the first attempt, successful
139    /// or not.
140    ///
141    /// `POST /api/v1/auth/oauth/exchange`
142    pub async fn exchange_o_auth_app_code(&self, body: &models::OAuthAppExchangeRequest) -> Result<models::OAuthAppExchangeResponse> {
143        self.client
144            .request_json(Request {
145                method: Method::POST,
146                path: "/api/v1/auth/oauth/exchange".to_string(),
147                query: NO_QUERY,
148                body: Some(body),
149                headers: Vec::new(),
150                idempotent: true,
151            })
152            .await
153    }
154
155    /// Get the calling user + tenant context
156    ///
157    /// Single source of truth for the browser to render account state, role badges, and the tenant
158    /// header. Resolves the user via api-key.user_id, then JWT `sub` claim, then
159    /// tenant_primary_email lookup.
160    ///
161    /// **Read `role` from the top level, never from `user.role`.** The top-level field is required
162    /// and always present; it resolves as `user?.role ?? extractRole(auth)`, so it answers for
163    /// every caller. `user` is nullable and NOT required — it is null whenever the caller
164    /// authenticated with an api key rather than a signed-in session, and the nested `role` exists
165    /// only in the other case.
166    ///
167    /// Spelled out because the schema alone did not prevent it: two mobile clients independently
168    /// modelled the response as `{user, tenant}`, read `user.role`, and so could not tell an owner
169    /// from a viewer for any key-authed caller — one of them had gated no settings on role at all
170    /// as a result. A client tested only against a JWT session sees `user.role` work perfectly and
171    /// ships. `scopes` and `auth_method` are required for the same reason and answer for every
172    /// caller too.
173    ///
174    /// `GET /api/v1/me`
175    pub async fn get_me(&self) -> Result<models::GetMeResponse> {
176        self.client
177            .request_json(Request {
178                method: Method::GET,
179                path: "/api/v1/me".to_string(),
180                query: NO_QUERY,
181                body: NO_BODY,
182                headers: Vec::new(),
183                idempotent: false,
184            })
185            .await
186    }
187
188    /// MFA enrolment status for the calling user
189    ///
190    /// `GET /api/v1/auth/mfa/status`
191    pub async fn get_mfa_status(&self) -> Result<models::GetMfaStatusResponse> {
192        self.client
193            .request_json(Request {
194                method: Method::GET,
195                path: "/api/v1/auth/mfa/status".to_string(),
196                query: NO_QUERY,
197                body: NO_BODY,
198                headers: Vec::new(),
199                idempotent: false,
200            })
201            .await
202    }
203
204    /// Sign in with Google — One Tap (FedCM) credential exchange
205    ///
206    /// The browser-side Google Identity Services (GSI) script renders a FedCM One Tap prompt on
207    /// `/login` and POSTs the resulting `credential` (a Google-signed JWT, RS256) here. Server
208    /// fetches Google JWKS (cached 24h), verifies signature/iss/aud/exp against the configured
209    /// Google OAuth client_id from admin KV, extracts `sub` + `email`, then mints a session via the
210    /// shared post-verify path. Same session shape the `/oauth/google/callback` redirect flow
211    /// returns.
212    ///
213    /// `POST /api/v1/auth/oauth/google/onetap`
214    pub async fn google_one_tap_auth(&self, body: &models::GoogleOneTapAuthRequest) -> Result<models::GoogleOneTapAuthResponse> {
215        self.client
216            .request_json(Request {
217                method: Method::POST,
218                path: "/api/v1/auth/oauth/google/onetap".to_string(),
219                query: NO_QUERY,
220                body: Some(body),
221                headers: Vec::new(),
222                idempotent: true,
223            })
224            .await
225    }
226
227    /// List linked OAuth identity providers + OTP fallback
228    ///
229    /// Returns one entry per supported login method (`otp`, `github`, `google`, `apple`) with
230    /// `linked: true|false`. Used by the Settings UI to render link/unlink buttons.
231    ///
232    /// `GET /api/v1/me/auth-providers`
233    pub async fn list_auth_providers(&self) -> Result<models::ListAuthProvidersResponse> {
234        self.client
235            .request_json(Request {
236                method: Method::GET,
237                path: "/api/v1/me/auth-providers".to_string(),
238                query: NO_QUERY,
239                body: NO_BODY,
240                headers: Vec::new(),
241                idempotent: false,
242            })
243            .await
244    }
245
246    /// List active api_keys (devices) for the calling user
247    ///
248    /// Returns the user's active api_keys across all devices (browser, iOS, CLI). `is_current`
249    /// flags the key making this call so UI can highlight 'this device'. Sorted by `last_used_at`
250    /// desc.
251    ///
252    /// `GET /api/v1/me/sessions`
253    pub async fn list_me_sessions(&self) -> Result<models::ListMeSessionsResponse> {
254        self.client
255            .request_json(Request {
256                method: Method::GET,
257                path: "/api/v1/me/sessions".to_string(),
258                query: NO_QUERY,
259                body: NO_BODY,
260                headers: Vec::new(),
261                idempotent: false,
262            })
263            .await
264    }
265
266    /// List OAuth identity providers (for login UI)
267    ///
268    /// Public, IP-rate-limited (60/min). Returns the list of configured OAuth login providers with
269    /// their enabled status — UI uses this to render only the buttons that will work. `apple` is
270    /// always advertised as enabled (App Store Guideline 4.8 placement requirement);
271    /// `github`/`google` enabled depends on whether super-admin has provisioned
272    /// client_id+client_secret.
273    ///
274    /// `GET /api/v1/auth/oauth/providers`
275    pub async fn list_o_auth_login_providers(&self) -> Result<models::OAuthLoginProvidersList> {
276        self.client
277            .request_json(Request {
278                method: Method::GET,
279                path: "/api/v1/auth/oauth/providers".to_string(),
280                query: NO_QUERY,
281                body: NO_BODY,
282                headers: Vec::new(),
283                idempotent: false,
284            })
285            .await
286    }
287
288    /// Revoke the calling api_key
289    ///
290    /// Revokes the api_key used to make this call. Idempotent — re-call returns 200 with
291    /// `already_revoked: true`. Cookie/JWT-only sessions return `{ok: true, key_id: null}` since
292    /// there's no server-side key to revoke.
293    ///
294    /// `POST /api/v1/auth/logout`
295    pub async fn logout(&self) -> Result<models::LogoutResponse> {
296        self.client
297            .request_json(Request {
298                method: Method::POST,
299                path: "/api/v1/auth/logout".to_string(),
300                query: NO_QUERY,
301                body: NO_BODY,
302                headers: Vec::new(),
303                idempotent: true,
304            })
305            .await
306    }
307
308    /// Mint a 60-second SSE token scoped to events:read
309    ///
310    /// Mints a short-lived (60 s) API key carrying only `events:read` scope, for use as the
311    /// `?token=` query param on browser SSE/WebSocket subscriptions which cannot set Authorization
312    /// headers. Token does not appear in the tenant key dashboard and auto-purges from KV.
313    ///
314    /// `POST /api/v1/auth/sse-tokens`
315    pub async fn mint_sse_token(&self) -> Result<models::MintSSETokenResponse> {
316        self.client
317            .request_json(Request {
318                method: Method::POST,
319                path: "/api/v1/auth/sse-tokens".to_string(),
320                query: NO_QUERY,
321                body: NO_BODY,
322                headers: Vec::new(),
323                idempotent: true,
324            })
325            .await
326    }
327
328    /// Register new user
329    ///
330    /// `POST /api/v1/register`
331    pub async fn register(&self, body: &serde_json::Map<String, serde_json::Value>) -> Result<serde_json::Map<String, serde_json::Value>> {
332        self.client
333            .request_json(Request {
334                method: Method::POST,
335                path: "/api/v1/register".to_string(),
336                query: NO_QUERY,
337                body: Some(body),
338                headers: Vec::new(),
339                idempotent: true,
340            })
341            .await
342    }
343
344    /// Request OTP code
345    ///
346    /// `POST /api/v1/auth/request-code`
347    pub async fn request_otp_code(&self, body: &serde_json::Map<String, serde_json::Value>) -> Result<serde_json::Map<String, serde_json::Value>> {
348        self.client
349            .request_json(Request {
350                method: Method::POST,
351                path: "/api/v1/auth/request-code".to_string(),
352                query: NO_QUERY,
353                body: Some(body),
354                headers: Vec::new(),
355                idempotent: true,
356            })
357            .await
358    }
359
360    /// Revoke a specific device session
361    ///
362    /// Revokes one of the user's api_keys. Refuses to revoke another user's key (403). Idempotent —
363    /// re-revoking a revoked key returns `{ok: true, already_revoked: true}`.
364    ///
365    /// `DELETE /api/v1/me/sessions/{keyId}`
366    pub async fn revoke_me_session(&self, key_id: &str) -> Result<models::RevokeMeSessionResponse> {
367        self.client
368            .request_json(Request {
369                method: Method::DELETE,
370                path: format!("/api/v1/me/sessions/{}", encode_path(key_id)),
371                query: NO_QUERY,
372                body: NO_BODY,
373                headers: Vec::new(),
374                idempotent: true,
375            })
376            .await
377    }
378
379    /// Begin browser OAuth login (302 to provider)
380    ///
381    /// Redirects the browser to GitHub/Google authorize URL. Server stores a one-shot state record
382    /// (10 min TTL) keyed by the `state` query the provider will echo back. PKCE codeVerifier is
383    /// generated server-side. `return_to` defaults to nothing (response is JSON); when set, must
384    /// satisfy the allow-list (same-origin paths, configured Universal-Link hosts, or `snaga://`).
385    /// `device_label` is captured here and surfaces on the minted `api_key` for `/me/sessions`.
386    ///
387    /// `GET /api/v1/auth/oauth/{provider}/start`
388    pub async fn start_o_auth_login(&self, provider: &models::OAuthLoginProviderConfigStatusProvider, params: &StartOAuthLoginParams) -> Result<serde_json::Value> {
389        self.client
390            .request_json(Request {
391                method: Method::GET,
392                path: format!("/api/v1/auth/oauth/{}/start", encode_path(&provider.to_string())),
393                query: Some(params),
394                body: NO_BODY,
395                headers: Vec::new(),
396                idempotent: false,
397            })
398            .await
399    }
400
401    /// Unlink an OAuth provider from the user
402    ///
403    /// Refuses to drop the user's last sign-in factor (counts other linked providers + OTP recovery
404    /// email). Idempotent — unlinking an already-unlinked provider returns 200 with
405    /// `already_unlinked: true`.
406    ///
407    /// `DELETE /api/v1/me/auth-providers/{provider}`
408    pub async fn unlink_auth_provider(&self, provider: &models::OAuthLoginProviderItemId) -> Result<models::UnlinkAuthProviderResponse> {
409        self.client
410            .request_json(Request {
411                method: Method::DELETE,
412                path: format!("/api/v1/me/auth-providers/{}", encode_path(&provider.to_string())),
413                query: NO_QUERY,
414                body: NO_BODY,
415                headers: Vec::new(),
416                idempotent: true,
417            })
418            .await
419    }
420
421    /// Verify email link
422    ///
423    /// `GET /api/v1/verify-email`
424    pub async fn verify_email(&self) -> Result<serde_json::Map<String, serde_json::Value>> {
425        self.client
426            .request_json(Request {
427                method: Method::GET,
428                path: "/api/v1/verify-email".to_string(),
429                query: NO_QUERY,
430                body: NO_BODY,
431                headers: Vec::new(),
432                idempotent: false,
433            })
434            .await
435    }
436
437    /// Verify a TOTP code
438    ///
439    /// Used both during enrolment confirmation and on subsequent logins. Burns no recovery code;
440    /// for recovery use `/auth/mfa/recovery`.
441    ///
442    /// `POST /api/v1/auth/mfa/verify`
443    pub async fn verify_mfa(&self, body: &models::VerifyMfaRequest) -> Result<models::VerifyMfaResponse> {
444        self.client
445            .request_json(Request {
446                method: Method::POST,
447                path: "/api/v1/auth/mfa/verify".to_string(),
448                query: NO_QUERY,
449                body: Some(body),
450                headers: Vec::new(),
451                idempotent: true,
452            })
453            .await
454    }
455
456    /// Verify a one-time recovery code (burns it on success)
457    ///
458    /// `POST /api/v1/auth/mfa/recovery`
459    pub async fn verify_mfa_recovery(&self, body: &models::VerifyMfaRecoveryRequest) -> Result<models::VerifyMfaRecoveryResponse> {
460        self.client
461            .request_json(Request {
462                method: Method::POST,
463                path: "/api/v1/auth/mfa/recovery".to_string(),
464                query: NO_QUERY,
465                body: Some(body),
466                headers: Vec::new(),
467                idempotent: true,
468            })
469            .await
470    }
471
472    /// Verify OTP code
473    ///
474    /// Expects body with email and code only (no request_id). Returns api_key to use as Bearer
475    /// token.
476    ///
477    /// `POST /api/v1/auth/verify-code`
478    pub async fn verify_otp_code(&self, body: &models::AuthVerifyCodeRequest) -> Result<models::AuthVerifyCodeResponse> {
479        self.client
480            .request_json(Request {
481                method: Method::POST,
482                path: "/api/v1/auth/verify-code".to_string(),
483                query: NO_QUERY,
484                body: Some(body),
485                headers: Vec::new(),
486                idempotent: true,
487            })
488            .await
489    }
490}