diff --git a/crates/api_clients/artcraft/artcraft_client/AGENTS.md b/crates/api_clients/artcraft/artcraft_client/AGENTS.md new file mode 100644 index 0000000000..759ae5aab0 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/AGENTS.md @@ -0,0 +1,27 @@ +# artcraft_client + +Rust HTTP endpoint bindings used by the ArtCraft Tauri desktop app. + +## Endpoint layout + +- Each HTTP endpoint binding must live in its own descriptively named `.rs` file. + This applies to both free functions and methods implemented on a shared client. + Do not combine several routes into a catch-all file such as `login_challenges.rs` + or `password_auth.rs`. +- Group related bindings in a directory module. Keep `mod.rs` for module declarations; + it must not contain endpoint implementations. For example: + `endpoints/users/login_challenges/create_login_challenge.rs` and + `endpoints/users/login_challenges/poll_login_challenge.rs`. +- Keep each endpoint's route constant, request construction, and endpoint-specific + request/response types in its file. Keep shared wire types in `artcraft_api_defs` + or a dedicated shared type module; do not duplicate them across endpoint files. +- Put shared HTTP transport, client setup, and common error handling in a separate + utility module. A transport module must not accumulate endpoint bindings. +- Update callers to import types from their new modules when splitting bindings. + Preserve request methods, paths, headers, error behavior, and cookie handling. + +## Validation + +Run `cargo check --offline -p artcraft_client` for client changes. For native login +bindings, also run `SQLX_OFFLINE=true cargo test --offline -p artcraft --lib +login_bridge_tests`; these tests use local HTTP fixtures and temporary cookie jars. diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/create_login_challenge.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/create_login_challenge.rs new file mode 100644 index 0000000000..44feaf28ed --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/create_login_challenge.rs @@ -0,0 +1,16 @@ +use artcraft_api_defs::users::login_challenges::{CreateLoginChallengeRequest, CreateLoginChallengeResponse}; +use reqwest::Method; + +use crate::utils::login_challenge_client::{LoginChallengeClient, LoginChallengeClientError}; + +pub const CREATE_PATH: &str = "/v1/login_challenges/create"; + +impl LoginChallengeClient { + pub async fn create(&self) -> Result { + let request = self + .request(Method::POST, CREATE_PATH) + .json(&CreateLoginChallengeRequest {}); + let (result, _) = self.send(request, CREATE_PATH).await?; + Ok(result) + } +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/mod.rs new file mode 100644 index 0000000000..18a2a278ee --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/mod.rs @@ -0,0 +1,2 @@ +pub mod create_login_challenge; +pub mod poll_login_challenge; diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/poll_login_challenge.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/poll_login_challenge.rs new file mode 100644 index 0000000000..89ce499356 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges/poll_login_challenge.rs @@ -0,0 +1,30 @@ +use artcraft_api_defs::users::login_challenges::{LoginChallengeResponse, PollLoginChallengeRequest}; +use reqwest::Method; + +use crate::utils::login_challenge_client::{LoginChallengeClient, LoginChallengeClientError}; + +pub const POLL_PATH: &str = "/v1/login_challenges/poll"; + +// Deliberately no Debug/Serialize: the wire result contains a bearer credential. +pub struct PollLoginChallengeResult { + pub response: LoginChallengeResponse, + pub session_set_cookie: Option, +} + +impl LoginChallengeClient { + pub async fn poll( + &self, + device_token: &str, + ) -> Result { + let request = self + .request(Method::POST, POLL_PATH) + .json(&PollLoginChallengeRequest { + device_token: device_token.to_owned(), + }); + let (response, session_set_cookie) = self.send(request, POLL_PATH).await?; + Ok(PollLoginChallengeResult { + response, + session_set_cookie, + }) + } +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs index e391a7f44a..9311baced0 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs @@ -1,2 +1,5 @@ pub mod login_challenges; -pub mod password_auth; +pub mod password_auth_response; +pub mod password_login; +pub mod password_signup; +pub mod session_info; diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth.rs deleted file mode 100644 index 6804164b9e..0000000000 --- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth.rs +++ /dev/null @@ -1,38 +0,0 @@ -use reqwest::Method; -use serde::{Deserialize, Serialize}; - -use super::login_challenges::{LoginChallengeClient, LoginChallengeClientError}; - -// No Debug: these request/response types contain credentials. -#[derive(Deserialize, Serialize)] -pub struct PasswordLoginRequest { - pub username_or_email: String, - pub password: String, -} - -#[derive(Deserialize, Serialize)] -pub struct PasswordSignupRequest { - pub username: String, - pub email_address: String, - pub password: String, - pub password_confirmation: String, - pub signup_source: String, -} - -#[derive(Deserialize)] -pub struct PasswordAuthResponse { - pub success: bool, - pub signed_session: Option, -} - -impl LoginChallengeClient { - pub async fn password_login(&self, request: &PasswordLoginRequest) -> Result<(PasswordAuthResponse, Option), LoginChallengeClientError> { - let path = "/v1/login"; - self.send(self.request(Method::POST, path).json(request), path).await - } - - pub async fn password_signup(&self, request: &PasswordSignupRequest) -> Result<(PasswordAuthResponse, Option), LoginChallengeClientError> { - let path = "/v1/create_account"; - self.send(self.request(Method::POST, path).json(request), path).await - } -} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth_response.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth_response.rs new file mode 100644 index 0000000000..aa0e8345ab --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth_response.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +// No Debug: the response contains a bearer credential. +#[derive(Deserialize)] +pub struct PasswordAuthResponse { + pub success: bool, + pub signed_session: Option, +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_login.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_login.rs new file mode 100644 index 0000000000..1cd7614c95 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_login.rs @@ -0,0 +1,20 @@ +use reqwest::Method; +use serde::{Deserialize, Serialize}; + +use crate::utils::login_challenge_client::{LoginChallengeClient, LoginChallengeClientError}; +use super::password_auth_response::PasswordAuthResponse; + +pub const LOGIN_PATH: &str = "/v1/login"; + +// No Debug: the request contains credentials. +#[derive(Deserialize, Serialize)] +pub struct PasswordLoginRequest { + pub username_or_email: String, + pub password: String, +} + +impl LoginChallengeClient { + pub async fn password_login(&self, request: &PasswordLoginRequest) -> Result<(PasswordAuthResponse, Option), LoginChallengeClientError> { + self.send(self.request(Method::POST, LOGIN_PATH).json(request), LOGIN_PATH).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_signup.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_signup.rs new file mode 100644 index 0000000000..87e2c0de0e --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_signup.rs @@ -0,0 +1,23 @@ +use reqwest::Method; +use serde::{Deserialize, Serialize}; + +use crate::utils::login_challenge_client::{LoginChallengeClient, LoginChallengeClientError}; +use super::password_auth_response::PasswordAuthResponse; + +pub const SIGNUP_PATH: &str = "/v1/create_account"; + +// No Debug: the request contains credentials. +#[derive(Deserialize, Serialize)] +pub struct PasswordSignupRequest { + pub username: String, + pub email_address: String, + pub password: String, + pub password_confirmation: String, + pub signup_source: String, +} + +impl LoginChallengeClient { + pub async fn password_signup(&self, request: &PasswordSignupRequest) -> Result<(PasswordAuthResponse, Option), LoginChallengeClientError> { + self.send(self.request(Method::POST, SIGNUP_PATH).json(request), SIGNUP_PATH).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/session_info.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/session_info.rs new file mode 100644 index 0000000000..8df2a3c51f --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/session_info.rs @@ -0,0 +1,19 @@ +use artcraft_api_defs::users::session_info::SessionInfoSuccessResponse; +use reqwest::Method; + +use crate::utils::login_challenge_client::{LoginChallengeClient, LoginChallengeClientError}; + +pub const SESSION_PATH: &str = "/v1/session"; + +impl LoginChallengeClient { + pub async fn session( + &self, + signed_session: &str, + ) -> Result { + let request = self + .request(Method::GET, SESSION_PATH) + .header("Cookie", format!("session={signed_session}")); + let (result, _) = self.send(request, SESSION_PATH).await?; + Ok(result) + } +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges.rs b/crates/api_clients/artcraft/artcraft_client/src/utils/login_challenge_client.rs similarity index 72% rename from crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges.rs rename to crates/api_clients/artcraft/artcraft_client/src/utils/login_challenge_client.rs index dbdf063f6e..b6c8ffe5c7 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/utils/login_challenge_client.rs @@ -1,10 +1,5 @@ use std::time::Duration; -use artcraft_api_defs::users::login_challenges::{ - CreateLoginChallengeRequest, CreateLoginChallengeResponse, LoginChallengeResponse, - PollLoginChallengeRequest, -}; -use artcraft_api_defs::users::session_info::SessionInfoSuccessResponse; use log::{info, warn}; use reqwest::header::SET_COOKIE; use reqwest::{Client, Method, RequestBuilder}; @@ -14,10 +9,6 @@ use url::Url; use crate::utils::api_host::ApiHost; use crate::utils::constants::USER_AGENT; -pub const CREATE_PATH: &str = "/v1/login_challenges/create"; -pub const POLL_PATH: &str = "/v1/login_challenges/poll"; -pub const SESSION_PATH: &str = "/v1/session"; - /// Native transport for the session bridge. No request/response bodies, cookies, /// or challenge credentials are logged, even at debug level. Never follows redirects. #[derive(Clone)] @@ -26,12 +17,6 @@ pub struct LoginChallengeClient { api_host: ApiHost, } -// Deliberately no Debug/Serialize: the wire result contains a bearer credential. -pub struct PollLoginChallengeResult { - pub response: LoginChallengeResponse, - pub session_set_cookie: Option, -} - #[derive(Debug)] pub struct LoginChallengeClientError { pub status: Option, @@ -92,41 +77,6 @@ impl LoginChallengeClient { .unwrap_or(false) } - pub async fn create(&self) -> Result { - let request = self - .request(Method::POST, CREATE_PATH) - .json(&CreateLoginChallengeRequest {}); - let (result, _) = self.send(request, CREATE_PATH).await?; - Ok(result) - } - - pub async fn poll( - &self, - device_token: &str, - ) -> Result { - let request = self - .request(Method::POST, POLL_PATH) - .json(&PollLoginChallengeRequest { - device_token: device_token.to_owned(), - }); - let (response, session_set_cookie) = self.send(request, POLL_PATH).await?; - Ok(PollLoginChallengeResult { - response, - session_set_cookie, - }) - } - - pub async fn session( - &self, - signed_session: &str, - ) -> Result { - let request = self - .request(Method::GET, SESSION_PATH) - .header("Cookie", format!("session={signed_session}")); - let (result, _) = self.send(request, SESSION_PATH).await?; - Ok(result) - } - pub(crate) fn request(&self, method: Method, path: &str) -> RequestBuilder { self .client diff --git a/crates/api_clients/artcraft/artcraft_client/src/utils/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/utils/mod.rs index 070f199125..8cfd5064ed 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/utils/mod.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/utils/mod.rs @@ -7,3 +7,4 @@ pub (crate) mod http_get_anonymous; pub mod api_host; pub mod constants; pub mod status_codes; +pub mod login_challenge_client; diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs index 23469d5457..794523c9ad 100644 --- a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs +++ b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs @@ -3,11 +3,12 @@ use std::sync::Arc; use artcraft_api_defs::users::login_challenges::{LoginChallengeFailure, LoginChallengeState}; use artcraft_api_defs::users::session_info::SessionUserInfo; -use artcraft_client::endpoints::users::login_challenges::{ +use artcraft_client::utils::login_challenge_client::{ LoginChallengeClient, LoginChallengeClientError, }; use artcraft_client::utils::api_host::ApiHost; -use artcraft_client::endpoints::users::password_auth::{PasswordLoginRequest, PasswordSignupRequest}; +use artcraft_client::endpoints::users::password_login::PasswordLoginRequest; +use artcraft_client::endpoints::users::password_signup::PasswordSignupRequest; use chrono::{DateTime, Utc}; use log::{info, warn}; use serde::Serialize;