max / synckit-client
4 files changed,
+63 insertions,
-32 deletions
| @@ -24,9 +24,9 @@ impl SyncKitClient { | |||
| 24 | 24 | password: &str, | |
| 25 | 25 | ) -> Result<(Uuid, Uuid)> { | |
| 26 | 26 | let body = Bytes::from(serde_json::to_vec(&AuthRequest { | |
| 27 | - | email: email.to_string(), | |
| 28 | - | password: password.to_string(), | |
| 29 | - | api_key: self.config.api_key.clone(), | |
| 27 | + | email, | |
| 28 | + | password, | |
| 29 | + | api_key: &self.config.api_key, | |
| 30 | 30 | })?); | |
| 31 | 31 | ||
| 32 | 32 | let resp = self | |
| @@ -139,11 +139,11 @@ impl SyncKitClient { | |||
| 139 | 139 | let redirect_uri = format!("http://127.0.0.1:{}/", redirect_port); | |
| 140 | 140 | ||
| 141 | 141 | let body = Bytes::from(serde_json::to_vec(&OAuthTokenRequest { | |
| 142 | - | grant_type: "authorization_code".to_string(), | |
| 143 | - | code: code.to_string(), | |
| 144 | - | redirect_uri, | |
| 145 | - | code_verifier: code_verifier.to_string(), | |
| 146 | - | client_id: self.config.api_key.clone(), | |
| 142 | + | grant_type: "authorization_code", | |
| 143 | + | code, | |
| 144 | + | redirect_uri: &redirect_uri, | |
| 145 | + | code_verifier, | |
| 146 | + | client_id: &self.config.api_key, | |
| 147 | 147 | })?); | |
| 148 | 148 | ||
| 149 | 149 | let resp = self | |
| @@ -222,7 +222,7 @@ mod tests { | |||
| 222 | 222 | client.restore_session("fake-token", user_id, app_id).unwrap(); | |
| 223 | 223 | ||
| 224 | 224 | let info = client.session_info().unwrap().expect("session should exist"); | |
| 225 | - | assert_eq!(info.token, "fake-token"); | |
| 225 | + | assert_eq!(*info.token, "fake-token"); | |
| 226 | 226 | assert_eq!(info.user_id, user_id); | |
| 227 | 227 | assert_eq!(info.app_id, app_id); | |
| 228 | 228 | } | |
| @@ -236,7 +236,7 @@ mod tests { | |||
| 236 | 236 | client.restore_session("second-token", user_id, app_id).unwrap(); | |
| 237 | 237 | ||
| 238 | 238 | let info = client.session_info().unwrap().unwrap(); | |
| 239 | - | assert_eq!(info.token, "second-token"); | |
| 239 | + | assert_eq!(*info.token, "second-token"); | |
| 240 | 240 | } | |
| 241 | 241 | ||
| 242 | 242 | // ── build_authorize_url ── | |
| @@ -352,11 +352,11 @@ mod tests { | |||
| 352 | 352 | #[test] | |
| 353 | 353 | fn oauth_token_request_serialization() { | |
| 354 | 354 | let req = OAuthTokenRequest { | |
| 355 | - | grant_type: "authorization_code".to_string(), | |
| 356 | - | code: "auth-code-123".to_string(), | |
| 357 | - | redirect_uri: "http://127.0.0.1:8080/".to_string(), | |
| 358 | - | code_verifier: "verifier-abc".to_string(), | |
| 359 | - | client_id: "api-key".to_string(), | |
| 355 | + | grant_type: "authorization_code", | |
| 356 | + | code: "auth-code-123", | |
| 357 | + | redirect_uri: "http://127.0.0.1:8080/", | |
| 358 | + | code_verifier: "verifier-abc", | |
| 359 | + | client_id: "api-key", | |
| 360 | 360 | }; | |
| 361 | 361 | ||
| 362 | 362 | let json = serde_json::to_string(&req).unwrap(); | |
| @@ -387,9 +387,9 @@ mod tests { | |||
| 387 | 387 | #[test] | |
| 388 | 388 | fn auth_request_serialization() { | |
| 389 | 389 | let req = AuthRequest { | |
| 390 | - | email: "user@example.com".to_string(), | |
| 391 | - | password: "secret123".to_string(), | |
| 392 | - | api_key: "ak_test".to_string(), | |
| 390 | + | email: "user@example.com", | |
| 391 | + | password: "secret123", | |
| 392 | + | api_key: "ak_test", | |
| 393 | 393 | }; | |
| 394 | 394 | ||
| 395 | 395 | let json = serde_json::to_string(&req).unwrap(); |
| @@ -127,8 +127,8 @@ struct Session { | |||
| 127 | 127 | ||
| 128 | 128 | /// Public session info returned by `session_info()`. | |
| 129 | 129 | pub struct SessionInfo { | |
| 130 | - | /// The JWT bearer token for API requests. | |
| 131 | - | pub token: String, | |
| 130 | + | /// The JWT bearer token for API requests (shared ref-counted to avoid cloning). | |
| 131 | + | pub token: Arc<String>, | |
| 132 | 132 | /// The authenticated user's UUID. | |
| 133 | 133 | pub user_id: Uuid, | |
| 134 | 134 | /// The SyncKit app UUID this session belongs to. | |
| @@ -192,7 +192,7 @@ impl SyncKitClient { | |||
| 192 | 192 | pub fn session_info(&self) -> Result<Option<SessionInfo>> { | |
| 193 | 193 | let guard = self.session.read(); | |
| 194 | 194 | Ok(guard.as_ref().map(|s| SessionInfo { | |
| 195 | - | token: (*s.token).clone(), | |
| 195 | + | token: Arc::clone(&s.token), | |
| 196 | 196 | user_id: s.user_id, | |
| 197 | 197 | app_id: s.app_id, | |
| 198 | 198 | })) | |
| @@ -250,6 +250,37 @@ impl SyncKitClient { | |||
| 250 | 250 | } | |
| 251 | 251 | } | |
| 252 | 252 | ||
| 253 | + | /// Validate an API key against a SyncKit server without constructing a full client. | |
| 254 | + | /// | |
| 255 | + | /// Returns the app name on success, or an error if the key is invalid or the server | |
| 256 | + | /// is unreachable. This is intended for setup UIs that need to verify a key before | |
| 257 | + | /// saving it. | |
| 258 | + | pub async fn validate_api_key(server_url: &str, api_key: &str) -> Result<String> { | |
| 259 | + | let url = format!("{server_url}/api/sync/validate-app"); | |
| 260 | + | let http = reqwest::Client::builder() | |
| 261 | + | .timeout(std::time::Duration::from_secs(10)) | |
| 262 | + | .build()?; | |
| 263 | + | let resp = http | |
| 264 | + | .get(&url) | |
| 265 | + | .query(&[("api_key", api_key)]) | |
| 266 | + | .send() | |
| 267 | + | .await?; | |
| 268 | + | let status = resp.status().as_u16(); | |
| 269 | + | if status == 401 { | |
| 270 | + | return Err(SyncKitError::Server { | |
| 271 | + | status: 401, | |
| 272 | + | message: "Invalid API key".to_string(), | |
| 273 | + | }); | |
| 274 | + | } | |
| 275 | + | let resp = helpers::check_response(resp).await?; | |
| 276 | + | #[derive(serde::Deserialize)] | |
| 277 | + | struct ValidateResponse { | |
| 278 | + | app_name: String, | |
| 279 | + | } | |
| 280 | + | let body: ValidateResponse = resp.json().await?; | |
| 281 | + | Ok(body.app_name) | |
| 282 | + | } | |
| 283 | + | ||
| 253 | 284 | #[cfg(test)] | |
| 254 | 285 | mod tests { | |
| 255 | 286 | use super::*; |
| @@ -48,6 +48,6 @@ pub mod keystore; | |||
| 48 | 48 | pub mod types; | |
| 49 | 49 | ||
| 50 | 50 | // Re-exports for convenience | |
| 51 | - | pub use client::{SessionInfo, SyncKitClient, SyncKitConfig}; | |
| 51 | + | pub use client::{validate_api_key, SessionInfo, SyncKitClient, SyncKitConfig}; | |
| 52 | 52 | pub use error::{Result, SyncKitError}; | |
| 53 | 53 | pub use types::{ChangeEntry, ChangeOp, Device, SyncStatus}; |
| @@ -47,10 +47,10 @@ impl ChangeOp { | |||
| 47 | 47 | // ── Auth ── | |
| 48 | 48 | ||
| 49 | 49 | #[derive(Serialize)] | |
| 50 | - | pub(crate) struct AuthRequest { | |
| 51 | - | pub email: String, | |
| 52 | - | pub password: String, | |
| 53 | - | pub api_key: String, | |
| 50 | + | pub(crate) struct AuthRequest<'a> { | |
| 51 | + | pub email: &'a str, | |
| 52 | + | pub password: &'a str, | |
| 53 | + | pub api_key: &'a str, | |
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | 56 | #[derive(Deserialize)] | |
| @@ -175,12 +175,12 @@ pub(crate) struct GetKeyResponse { | |||
| 175 | 175 | // ── OAuth ── | |
| 176 | 176 | ||
| 177 | 177 | #[derive(Serialize)] | |
| 178 | - | pub(crate) struct OAuthTokenRequest { | |
| 179 | - | pub grant_type: String, | |
| 180 | - | pub code: String, | |
| 181 | - | pub redirect_uri: String, | |
| 182 | - | pub code_verifier: String, | |
| 183 | - | pub client_id: String, | |
| 178 | + | pub(crate) struct OAuthTokenRequest<'a> { | |
| 179 | + | pub grant_type: &'a str, | |
| 180 | + | pub code: &'a str, | |
| 181 | + | pub redirect_uri: &'a str, | |
| 182 | + | pub code_verifier: &'a str, | |
| 183 | + | pub client_id: &'a str, | |
| 184 | 184 | } | |
| 185 | 185 | ||
| 186 | 186 | #[derive(Deserialize)] |