Skip to main content

uptrakit_openapi_client/
auth.rs

1use crate::Result;
2use crate::UptrakitClient;
3use crate::types_impl::auth::{
4    AuthResponse, LoginRequest, LogoutRequest, RefreshRequest, RefreshResponse, RegisterRequest,
5    UserResponse,
6};
7use crate::types_impl::device_auth::{DeviceAuthApproveRequest, DeviceAuthApproveResponse};
8use crate::types_impl::oauth::{
9    DeviceAuthDenyRequest, DeviceAuthDenyResponse, DeviceAuthLookupQuery, DeviceAuthLookupResponse,
10    DeviceAuthorizationRequest, DeviceAuthorizationResponse, OAuthAuthorizationServerMetadata,
11    OAuthTokenRequest, OAuthTokenResponse,
12};
13use crate::types_impl::oidc_auth::AuthMethodsResponse;
14
15impl UptrakitClient {
16    /// Register a new user account.
17    ///
18    /// This endpoint does not require authentication.
19    pub async fn register(&self, req: &RegisterRequest) -> Result<AuthResponse> {
20        self.post_json_unauth(crate::paths::auth::REGISTER, req)
21            .await
22    }
23
24    /// Log in with email and password.
25    ///
26    /// This endpoint does not require authentication.
27    pub async fn login(&self, req: &LoginRequest) -> Result<AuthResponse> {
28        self.post_json_unauth(crate::paths::auth::LOGIN, req).await
29    }
30
31    /// Refresh an access token using a refresh token.
32    ///
33    /// This endpoint does not require authentication.
34    pub async fn refresh(&self, req: &RefreshRequest) -> Result<RefreshResponse> {
35        self.post_json_unauth(crate::paths::auth::REFRESH, req)
36            .await
37    }
38
39    /// Log out by revoking a refresh token.
40    pub async fn logout(&self, req: &LogoutRequest) -> Result<()> {
41        self.post_json_no_content(crate::paths::auth::LOGOUT, req)
42            .await
43    }
44
45    /// List available authentication methods.
46    ///
47    /// This endpoint does not require authentication.
48    pub async fn auth_methods(&self) -> Result<AuthMethodsResponse> {
49        self.get_unauth(crate::paths::auth::METHODS).await
50    }
51
52    /// Approve a pending device authorization request.
53    pub async fn device_auth_approve(
54        &self,
55        req: &DeviceAuthApproveRequest,
56    ) -> Result<DeviceAuthApproveResponse> {
57        self.post_json(crate::paths::auth::DEVICE_APPROVE, req)
58            .await
59    }
60
61    /// Retrieve the current authenticated user's profile.
62    pub async fn me(&self) -> Result<UserResponse> {
63        self.get(crate::paths::auth::ME).await
64    }
65
66    /// Start an RFC 8628 device authorization flow.
67    ///
68    /// Per RFC 8628 §3.1. Returns the device_code, user_code, verification URIs,
69    /// expiry, and recommended polling interval. This endpoint does not require
70    /// authentication.
71    pub async fn oauth_device_authorization(
72        &self,
73        req: &DeviceAuthorizationRequest,
74    ) -> Result<DeviceAuthorizationResponse> {
75        self.post_form_unauth(crate::paths::oauth::DEVICE_AUTHORIZATION, req)
76            .await
77    }
78
79    /// Exchange a device_code for an access token.
80    ///
81    /// Per RFC 6749 §3.2 / RFC 8628 §3.4. Form-urlencoded body. On HTTP 400 the
82    /// caller receives `Err(ClientError::OAuthError(OAuthErrorResponse))` with
83    /// the typed `OAuthErrorCode`. This endpoint does not require
84    /// authentication.
85    pub async fn oauth_token(&self, req: &OAuthTokenRequest) -> Result<OAuthTokenResponse> {
86        self.post_form_unauth(crate::paths::oauth::TOKEN, req).await
87    }
88
89    /// Fetch the RFC 8414 §3 authorization server metadata document.
90    ///
91    /// Public; no authentication required.
92    pub async fn oauth_authorization_server_metadata(
93        &self,
94    ) -> Result<OAuthAuthorizationServerMetadata> {
95        self.get_unauth(crate::paths::oauth::METADATA).await
96    }
97
98    /// Deny a pending device authorization request (UI-internal).
99    pub async fn device_auth_deny(
100        &self,
101        req: &DeviceAuthDenyRequest,
102    ) -> Result<DeviceAuthDenyResponse> {
103        self.post_json(crate::paths::auth::DEVICE_DENY, req).await
104    }
105
106    /// Look up the `client_name` + `expires_at` for a pending flow.
107    ///
108    /// Authenticated; requires `CanViewServices`. Query parameters are
109    /// serialised by `reqwest::RequestBuilder::query` (which uses
110    /// `serde_urlencoded` internally) so no manual URL building is required.
111    pub async fn device_auth_lookup(
112        &self,
113        query: &DeviceAuthLookupQuery,
114    ) -> Result<DeviceAuthLookupResponse> {
115        self.get_with_query(crate::paths::auth::DEVICE_LOOKUP, query)
116            .await
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::types_impl::SecretString;
123    use crate::types_impl::auth::{LoginRequest, LogoutRequest, RefreshRequest, RegisterRequest};
124    use crate::types_impl::device_auth::DeviceAuthApproveRequest;
125    use crate::types_impl::oauth::{
126        DeviceAuthDenyRequest, DeviceAuthorizationRequest, OAuthTokenRequest,
127    };
128
129    #[test]
130    fn register_request_serialization() {
131        let req = RegisterRequest {
132            email: "admin@example.com".to_string(),
133            first_name: "Admin".to_string(),
134            last_name: "User".to_string(),
135            password: SecretString::new("SecurePass123"),
136            registration_token: None,
137        };
138        let json = serde_json::to_value(&req).expect("serialize");
139        assert_eq!(json["email"], "admin@example.com");
140        assert_eq!(json["first_name"], "Admin");
141        assert_eq!(json["last_name"], "User");
142        assert_eq!(json["password"], "SecurePass123");
143    }
144
145    #[test]
146    fn register_request_with_token_serialization() {
147        let req = RegisterRequest {
148            email: "admin@example.com".to_string(),
149            first_name: "Admin".to_string(),
150            last_name: "User".to_string(),
151            password: SecretString::new("SecurePass123"),
152            registration_token: Some(SecretString::new("invite-tok-abc")),
153        };
154        let json = serde_json::to_value(&req).expect("serialize");
155        assert_eq!(json["registration_token"], "invite-tok-abc");
156    }
157
158    #[test]
159    fn login_request_serialization() {
160        let req = LoginRequest {
161            email: "admin@example.com".to_string(),
162            password: SecretString::new("SecurePass123"),
163        };
164        let json = serde_json::to_value(&req).expect("serialize");
165        assert_eq!(json["email"], "admin@example.com");
166        assert_eq!(json["password"], "SecurePass123");
167    }
168
169    #[test]
170    fn logout_request_serialization() {
171        let req = LogoutRequest {
172            refresh_token: Some(SecretString::new("refresh-tok-xyz")),
173        };
174        let json = serde_json::to_value(&req).expect("serialize");
175        assert_eq!(json["refresh_token"], "refresh-tok-xyz");
176    }
177
178    #[test]
179    fn refresh_request_serialization() {
180        let req = RefreshRequest {
181            refresh_token: Some(SecretString::new("refresh-tok-xyz")),
182        };
183        let json = serde_json::to_value(&req).expect("serialize");
184        assert_eq!(json["refresh_token"], "refresh-tok-xyz");
185    }
186
187    #[test]
188    fn device_auth_approve_request_serialization() {
189        let req = DeviceAuthApproveRequest {
190            user_code: "ABCD-1234".to_string(),
191        };
192        let json = serde_json::to_value(&req).expect("serialize");
193        assert_eq!(json["user_code"], "ABCD-1234");
194    }
195
196    #[test]
197    fn device_authorization_request_form_serialization() {
198        use serde_urlencoded;
199        let req = DeviceAuthorizationRequest::new(
200            "uptrakit-cli".into(),
201            None,
202            Some("cli-host-2026-05-12".into()),
203        );
204        let encoded = serde_urlencoded::to_string(&req).expect("encode");
205        assert!(encoded.contains("client_id=uptrakit-cli"));
206        assert!(encoded.contains("client_name=cli-host-2026-05-12"));
207        assert!(!encoded.contains("scope="), "scope omitted when None");
208    }
209
210    #[test]
211    fn oauth_token_request_form_serialization() {
212        use serde_urlencoded;
213        let req = OAuthTokenRequest::new(
214            "urn:ietf:params:oauth:grant-type:device_code".into(),
215            Some("abc-123".into()),
216            Some("uptrakit-cli".into()),
217        );
218        let encoded = serde_urlencoded::to_string(&req).expect("encode");
219        assert!(
220            encoded.contains("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code")
221                || encoded.contains("grant_type=urn:ietf:params:oauth:grant-type:device_code"),
222            "grant_type URI preserved verbatim"
223        );
224        assert!(encoded.contains("device_code=abc-123"));
225        assert!(encoded.contains("client_id=uptrakit-cli"));
226    }
227
228    #[test]
229    fn device_auth_deny_request_serialization() {
230        let req = DeviceAuthDenyRequest::new("ABCD-EFGH".into());
231        let json = serde_json::to_value(&req).expect("serialize");
232        assert_eq!(json["user_code"], "ABCD-EFGH");
233    }
234}