use crate::error::SymbiosError;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct AtprotoCredentials {
pub pds_url: String,
pub identifier: String,
pub password: String,
}
#[derive(Resource, Debug, Clone, Serialize, Deserialize)]
pub struct AtprotoSession {
pub did: String,
pub handle: String,
pub access_jwt: String,
pub refresh_jwt: String,
}
#[derive(Serialize)]
struct CreateSessionRequest<'a> {
identifier: &'a str,
password: &'a str,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateSessionResponse {
did: String,
handle: String,
access_jwt: String,
refresh_jwt: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RefreshSessionResponse {
did: String,
handle: String,
access_jwt: String,
refresh_jwt: String,
}
#[derive(Deserialize)]
struct GetServiceAuthResponse {
token: String,
}
pub async fn get_service_auth(
client: &reqwest::Client,
session: &AtprotoSession,
pds_url: &str,
aud: &str,
) -> Result<String, SymbiosError> {
validate_pds_url(pds_url)?;
let url = format!(
"{}/xrpc/com.atproto.server.getServiceAuth",
pds_url.trim_end_matches('/')
);
let resp = client
.get(&url)
.query(&[("aud", aud)])
.header("Authorization", format!("Bearer {}", session.access_jwt))
.send()
.await?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(SymbiosError::AuthFailed(format!(
"getServiceAuth returned {status}: {body}"
)));
}
let response: GetServiceAuthResponse = resp.json().await?;
Ok(response.token)
}
fn validate_pds_url(raw: &str) -> Result<(), SymbiosError> {
let parsed = url::Url::parse(raw)
.map_err(|e| SymbiosError::AuthFailed(format!("invalid PDS URL '{raw}': {e}")))?;
if parsed.scheme() != "https" {
return Err(SymbiosError::AuthFailed(format!(
"PDS URL must use HTTPS to protect credentials, got: {raw}"
)));
}
if parsed.host().is_none() {
return Err(SymbiosError::AuthFailed(format!(
"PDS URL has no host: {raw}"
)));
}
Ok(())
}
pub async fn create_session(
client: &reqwest::Client,
credentials: &AtprotoCredentials,
) -> Result<AtprotoSession, SymbiosError> {
validate_pds_url(&credentials.pds_url)?;
let url = format!(
"{}/xrpc/com.atproto.server.createSession",
credentials.pds_url.trim_end_matches('/')
);
let resp = client
.post(&url)
.json(&CreateSessionRequest {
identifier: &credentials.identifier,
password: &credentials.password,
})
.send()
.await?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(SymbiosError::AuthFailed(format!(
"PDS returned {status}: {body}"
)));
}
let response: CreateSessionResponse = resp.json().await?;
Ok(AtprotoSession {
did: response.did,
handle: response.handle,
access_jwt: response.access_jwt,
refresh_jwt: response.refresh_jwt,
})
}
pub async fn refresh_session(
client: &reqwest::Client,
session: &AtprotoSession,
pds_url: &str,
) -> Result<AtprotoSession, SymbiosError> {
validate_pds_url(pds_url)?;
let url = format!(
"{}/xrpc/com.atproto.server.refreshSession",
pds_url.trim_end_matches('/')
);
let resp = client
.post(&url)
.header("Authorization", format!("Bearer {}", session.refresh_jwt))
.send()
.await?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(SymbiosError::AuthFailed(format!(
"PDS returned {status}: {body}"
)));
}
let response: RefreshSessionResponse = resp.json().await?;
Ok(AtprotoSession {
did: response.did,
handle: response.handle,
access_jwt: response.access_jwt,
refresh_jwt: response.refresh_jwt,
})
}