atproto-client 0.8.1

HTTP client for AT Protocol services with OAuth and identity integration
Documentation
//! AT Protocol server operations for session management.
//!
//! This module provides client functions for interacting with AT Protocol server endpoints,
//! including session creation and refresh operations using app passwords. Supports the
//! `com.atproto.server` XRPC methods for authentication and session management.
//!
//! ## Operations
//!
//! - **`create_session()`**: Create a new authentication session with app password
//! - **`refresh_session()`**: Refresh an existing authentication session
//! - **`create_app_password()`**: Create a new app password for authenticated account
//!
//! ## Request/Response Types
//!
//! - **`CreateSessionRequest`**: Parameters for creating a new session
//! - **`AppPasswordSession`**: Response containing session data and tokens
//! - **`RefreshSessionResponse`**: Response from session refresh operation
//! - **`AppPasswordResponse`**: Response containing created app password details
//!
//! ## Authentication
//!
//! Session creation uses app password authentication, while session refresh requires
//! the refresh JWT token from a previous session. App password creation requires
//! an access JWT token from an authenticated session.

use anyhow::Result;
use serde::{Deserialize, Serialize};

use crate::{client::post_json, url::URLBuilder};

/// Request to create a new authentication session.
#[derive(Debug, Serialize, Clone)]
pub struct CreateSessionRequest {
    /// Handle or other identifier supported by the server for the authenticating user
    pub identifier: String,
    /// User password or app password
    pub password: String,
    /// Optional two-factor authentication token
    #[serde(skip_serializing_if = "Option::is_none", rename = "authFactorToken")]
    pub auth_factor_token: Option<String>,
}

/// App password session data returned from successful authentication.
#[derive(Debug, Deserialize, Clone)]
pub struct AppPasswordSession {
    /// Distributed identifier for the authenticated account
    pub did: String,
    /// Handle for the authenticated account
    pub handle: String,
    /// Email address for the authenticated account
    pub email: String,
    /// JWT access token for authenticated requests
    #[serde(rename = "accessJwt")]
    pub access_jwt: String,
    /// JWT refresh token for obtaining new access tokens
    #[serde(rename = "refreshJwt")]
    pub refresh_jwt: String,
}

/// Response from refreshing an authentication session.
#[derive(Debug, Deserialize, Clone)]
pub struct RefreshSessionResponse {
    /// Distributed identifier for the authenticated account
    pub did: String,
    /// Handle for the authenticated account
    pub handle: String,
    /// JWT access token for authenticated requests
    #[serde(rename = "accessJwt")]
    pub access_jwt: String,
    /// JWT refresh token for obtaining new access tokens
    #[serde(rename = "refreshJwt")]
    pub refresh_jwt: String,
    /// Whether the account is active
    #[serde(skip_serializing_if = "Option::is_none")]
    pub active: Option<bool>,
    /// Account status (e.g., "takendown", "suspended", "deactivated")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
}

/// Response from creating a new app password.
#[derive(Debug, Deserialize, Clone)]
pub struct AppPasswordResponse {
    /// Name of the app password
    pub name: String,
    /// Generated app password string
    pub password: String,
    /// Creation timestamp in ISO 8601 format
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

/// Creates a new authentication session using app password credentials.
///
/// # Arguments
///
/// * `http_client` - HTTP client for making requests
/// * `base_url` - Base URL of the AT Protocol server
/// * `identifier` - Handle or other identifier for the user
/// * `password` - User password or app password
/// * `auth_factor_token` - Optional two-factor authentication token
///
/// # Returns
///
/// The created session data including access and refresh tokens
///
/// # Errors
///
/// Returns errors for HTTP request failures, authentication failures,
/// or JSON parsing failures.
pub async fn create_session(
    http_client: &reqwest::Client,
    base_url: &str,
    identifier: &str,
    password: &str,
    auth_factor_token: Option<&str>,
) -> Result<AppPasswordSession> {
    let mut url_builder = URLBuilder::new(base_url);
    url_builder.path("/xrpc/com.atproto.server.createSession");
    let url = url_builder.build();

    let request = CreateSessionRequest {
        identifier: identifier.to_string(),
        password: password.to_string(),
        auth_factor_token: auth_factor_token.map(|s| s.to_string()),
    };

    let value = serde_json::to_value(request)?;

    post_json(http_client, &url, value)
        .await
        .and_then(|value| serde_json::from_value(value).map_err(|err| err.into()))
}

/// Refreshes an existing authentication session using a refresh token.
///
/// # Arguments
///
/// * `http_client` - HTTP client for making requests
/// * `base_url` - Base URL of the AT Protocol server
/// * `refresh_token` - JWT refresh token from a previous session
///
/// # Returns
///
/// The refreshed session data with new access and refresh tokens
///
/// # Errors
///
/// Returns errors for HTTP request failures, authentication failures,
/// or JSON parsing failures.
pub async fn refresh_session(
    http_client: &reqwest::Client,
    base_url: &str,
    refresh_token: &str,
) -> Result<RefreshSessionResponse> {
    let mut url_builder = URLBuilder::new(base_url);
    url_builder.path("/xrpc/com.atproto.server.refreshSession");
    let url = url_builder.build();

    // Create a new client with the refresh token in Authorization header
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&format!("Bearer {}", refresh_token))?,
    );

    let response = http_client.post(&url).headers(headers).send().await?;

    let value = response.json::<serde_json::Value>().await?;

    serde_json::from_value(value).map_err(|err| err.into())
}

/// Creates a new app password for the authenticated account.
///
/// # Arguments
///
/// * `http_client` - HTTP client for making requests
/// * `base_url` - Base URL of the AT Protocol server
/// * `access_token` - JWT access token for authentication
/// * `name` - Name for the app password
///
/// # Returns
///
/// The created app password details including the generated password
///
/// # Errors
///
/// Returns errors for HTTP request failures, authentication failures,
/// or JSON parsing failures.
pub async fn create_app_password(
    http_client: &reqwest::Client,
    base_url: &str,
    access_token: &str,
    name: &str,
) -> Result<AppPasswordResponse> {
    let mut url_builder = URLBuilder::new(base_url);
    url_builder.path("/xrpc/com.atproto.server.createAppPassword");
    let url = url_builder.build();

    let request_body = serde_json::json!({
        "name": name
    });

    // Create a new client with the access token in Authorization header
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&format!("Bearer {}", access_token))?,
    );

    let response = http_client
        .post(&url)
        .headers(headers)
        .json(&request_body)
        .send()
        .await?;

    let value = response.json::<serde_json::Value>().await?;

    serde_json::from_value(value).map_err(|err| err.into())
}