split bindings

This commit is contained in:
Brandon Thomas
2026-09-26 03:07:45 -07:00
parent c1079ce5ed
commit 45c5d0d336
13 changed files with 153 additions and 91 deletions
@@ -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.
@@ -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<CreateLoginChallengeResponse, LoginChallengeClientError> {
let request = self
.request(Method::POST, CREATE_PATH)
.json(&CreateLoginChallengeRequest {});
let (result, _) = self.send(request, CREATE_PATH).await?;
Ok(result)
}
}
@@ -0,0 +1,2 @@
pub mod create_login_challenge;
pub mod poll_login_challenge;
@@ -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<String>,
}
impl LoginChallengeClient {
pub async fn poll(
&self,
device_token: &str,
) -> Result<PollLoginChallengeResult, LoginChallengeClientError> {
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,
})
}
}
@@ -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;
@@ -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<String>,
}
impl LoginChallengeClient {
pub async fn password_login(&self, request: &PasswordLoginRequest) -> Result<(PasswordAuthResponse, Option<String>), 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<String>), LoginChallengeClientError> {
let path = "/v1/create_account";
self.send(self.request(Method::POST, path).json(request), path).await
}
}
@@ -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<String>,
}
@@ -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<String>), LoginChallengeClientError> {
self.send(self.request(Method::POST, LOGIN_PATH).json(request), LOGIN_PATH).await
}
}
@@ -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<String>), LoginChallengeClientError> {
self.send(self.request(Method::POST, SIGNUP_PATH).json(request), SIGNUP_PATH).await
}
}
@@ -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<SessionInfoSuccessResponse, LoginChallengeClientError> {
let request = self
.request(Method::GET, SESSION_PATH)
.header("Cookie", format!("session={signed_session}"));
let (result, _) = self.send(request, SESSION_PATH).await?;
Ok(result)
}
}
@@ -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<String>,
}
#[derive(Debug)]
pub struct LoginChallengeClientError {
pub status: Option<u16>,
@@ -92,41 +77,6 @@ impl LoginChallengeClient {
.unwrap_or(false)
}
pub async fn create(&self) -> Result<CreateLoginChallengeResponse, LoginChallengeClientError> {
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<PollLoginChallengeResult, LoginChallengeClientError> {
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<SessionInfoSuccessResponse, LoginChallengeClientError> {
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
@@ -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;
@@ -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;