pub mod create_client; pub mod delete_client; pub mod get_client; pub mod list_clients; pub use create_client::create_api_client; pub use delete_client::delete_api_client; pub use get_client::get_api_client; pub use list_clients::list_api_clients; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiClientView { pub id: String, pub name: String, pub client_key: String, pub client_id_url: String, pub client_uri: String, pub redirect_uris: Vec, pub client_type: String, pub scopes: String, pub allowed_origins: Vec, pub is_active: bool, pub created_at: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiClientViewWithSecret { #[serde(flatten)] pub client: ApiClientView, #[serde(skip_serializing_if = "Option::is_none")] pub client_secret: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateApiClientInput { pub name: String, pub client_id_url: String, pub client_uri: String, pub redirect_uris: Vec, #[serde(default = "default_client_type")] pub client_type: String, #[serde(default = "default_scopes")] pub scopes: String, pub allowed_origins: Option>, } fn default_client_type() -> String { "confidential".to_string() } fn default_scopes() -> String { "atproto".to_string() } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DeleteApiClientInput { pub id: String, } /// Build an ApiClientView from a database row. /// /// Note: `is_active` is stored as i32 in the database (SQLite/Postgres compat via AnyPool). /// The existing codebase uses `row.get()` with the `sqlx::Row` trait. We follow the same /// pattern here but use `try_get` to return a Result rather than panic on missing columns. pub fn row_to_view(row: &sqlx::any::AnyRow) -> Result { use sqlx::Row; let redirect_uris_raw: String = row.try_get("redirect_uris")?; let redirect_uris: Vec = serde_json::from_str(&redirect_uris_raw).unwrap_or_default(); let allowed_origins_raw: Option = row.try_get("allowed_origins")?; let allowed_origins: Vec = allowed_origins_raw .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default(); // is_active is stored as i32 (0/1) for SQLite/Postgres compatibility, // matching the pattern used throughout admin/api_clients.rs. let is_active_raw: i32 = row.try_get("is_active")?; Ok(ApiClientView { id: row.try_get("id")?, name: row.try_get("name")?, client_key: row.try_get("client_key")?, client_id_url: row.try_get("client_id_url")?, client_uri: row.try_get("client_uri")?, redirect_uris, client_type: row.try_get("client_type")?, scopes: row.try_get("scopes")?, allowed_origins, is_active: is_active_raw != 0, created_at: row.try_get("created_at")?, }) }