feat: KratosClient — identity management (30 endpoints)
Typed Kratos admin API client covering identities, sessions, recovery, schemas, courier messages, and health checks. Bump: sunbeam-sdk v0.3.0
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3591,7 +3591,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Sunbeam SDK — reusable library for cluster management"
|
description = "Sunbeam SDK — reusable library for cluster management"
|
||||||
repository = "https://src.sunbeam.pt/studio/cli"
|
repository = "https://src.sunbeam.pt/studio/cli"
|
||||||
|
|||||||
378
sunbeam-sdk/src/identity/mod.rs
Normal file
378
sunbeam-sdk/src/identity/mod.rs
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
//! Kratos identity management client.
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use types::*;
|
||||||
|
|
||||||
|
/// Client for the Ory Kratos Admin API.
|
||||||
|
pub struct KratosClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for KratosClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"kratos"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KratosClient {
|
||||||
|
/// Build a KratosClient from domain (e.g. `https://id.{domain}`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://id.{domain}");
|
||||||
|
Self::from_parts(base_url, AuthMethod::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Identities ---------------------------------------------------------
|
||||||
|
|
||||||
|
/// List identities with optional pagination.
|
||||||
|
pub async fn list_identities(
|
||||||
|
&self,
|
||||||
|
page: Option<u32>,
|
||||||
|
page_size: Option<u32>,
|
||||||
|
) -> Result<Vec<Identity>> {
|
||||||
|
let page = page.unwrap_or(1);
|
||||||
|
let size = page_size.unwrap_or(20);
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/identities?page={page}&page_size={size}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos list identities",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new identity.
|
||||||
|
pub async fn create_identity(&self, body: &CreateIdentityBody) -> Result<Identity> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "admin/identities", Some(body), "kratos create identity")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single identity by ID.
|
||||||
|
pub async fn get_identity(&self, id: &str) -> Result<Identity> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/identities/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos get identity",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an identity (full replace).
|
||||||
|
pub async fn update_identity(&self, id: &str, body: &UpdateIdentityBody) -> Result<Identity> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/identities/{id}"),
|
||||||
|
Some(body),
|
||||||
|
"kratos update identity",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Patch an identity (partial update).
|
||||||
|
pub async fn patch_identity(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
patches: &[serde_json::Value],
|
||||||
|
) -> Result<Identity> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("admin/identities/{id}"),
|
||||||
|
Some(&patches),
|
||||||
|
"kratos patch identity",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete an identity.
|
||||||
|
pub async fn delete_identity(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/identities/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos delete identity",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batch patch identities.
|
||||||
|
pub async fn batch_patch_identities(
|
||||||
|
&self,
|
||||||
|
body: &BatchPatchIdentitiesBody,
|
||||||
|
) -> Result<BatchPatchResult> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::PATCH, "admin/identities", Some(body), "kratos batch patch")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get identity by external credential identifier (e.g. email).
|
||||||
|
pub async fn get_by_credential_identifier(&self, identifier: &str) -> Result<Vec<Identity>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!(
|
||||||
|
"admin/identities?credentials_identifier={}&page_size=1",
|
||||||
|
identifier
|
||||||
|
),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos get by credential",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a specific credential from an identity.
|
||||||
|
pub async fn delete_credential(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
credential_type: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/identities/{id}/credentials/{credential_type}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos delete credential",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Sessions -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List all sessions across identities.
|
||||||
|
pub async fn list_sessions(
|
||||||
|
&self,
|
||||||
|
page_size: Option<u32>,
|
||||||
|
page_token: Option<&str>,
|
||||||
|
active: Option<bool>,
|
||||||
|
) -> Result<Vec<Session>> {
|
||||||
|
let mut path = format!(
|
||||||
|
"admin/sessions?page_size={}",
|
||||||
|
page_size.unwrap_or(20)
|
||||||
|
);
|
||||||
|
if let Some(token) = page_token {
|
||||||
|
path.push_str(&format!("&page_token={token}"));
|
||||||
|
}
|
||||||
|
if let Some(active) = active {
|
||||||
|
path.push_str(&format!("&active={active}"));
|
||||||
|
}
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "kratos list sessions")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific session.
|
||||||
|
pub async fn get_session(&self, id: &str) -> Result<Session> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/sessions/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos get session",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extend a session.
|
||||||
|
pub async fn extend_session(&self, id: &str) -> Result<Session> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("admin/sessions/{id}/extend"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos extend session",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable (revoke) a session.
|
||||||
|
pub async fn disable_session(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/sessions/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos disable session",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List sessions for a specific identity.
|
||||||
|
pub async fn list_identity_sessions(&self, identity_id: &str) -> Result<Vec<Session>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/identities/{identity_id}/sessions"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos list identity sessions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all sessions for a specific identity.
|
||||||
|
pub async fn delete_identity_sessions(&self, identity_id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/identities/{identity_id}/sessions"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos delete identity sessions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Recovery -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// Create a recovery code for an identity.
|
||||||
|
pub async fn create_recovery_code(
|
||||||
|
&self,
|
||||||
|
identity_id: &str,
|
||||||
|
expires_in: Option<&str>,
|
||||||
|
) -> Result<RecoveryCodeResult> {
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"identity_id": identity_id,
|
||||||
|
"expires_in": expires_in.unwrap_or("24h"),
|
||||||
|
});
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "admin/recovery/code", Some(&body), "kratos recovery code")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a recovery link for an identity.
|
||||||
|
pub async fn create_recovery_link(
|
||||||
|
&self,
|
||||||
|
identity_id: &str,
|
||||||
|
expires_in: Option<&str>,
|
||||||
|
) -> Result<RecoveryLinkResult> {
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"identity_id": identity_id,
|
||||||
|
"expires_in": expires_in.unwrap_or("24h"),
|
||||||
|
});
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "admin/recovery/link", Some(&body), "kratos recovery link")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Schemas ------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List identity schemas.
|
||||||
|
pub async fn list_schemas(&self) -> Result<Vec<IdentitySchema>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"schemas",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos list schemas",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific identity schema.
|
||||||
|
pub async fn get_schema(&self, id: &str) -> Result<serde_json::Value> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("schemas/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos get schema",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Courier messages ---------------------------------------------------
|
||||||
|
|
||||||
|
/// List courier messages.
|
||||||
|
pub async fn list_courier_messages(
|
||||||
|
&self,
|
||||||
|
page_size: Option<u32>,
|
||||||
|
page_token: Option<&str>,
|
||||||
|
) -> Result<Vec<CourierMessage>> {
|
||||||
|
let mut path = format!(
|
||||||
|
"admin/courier/messages?page_size={}",
|
||||||
|
page_size.unwrap_or(20)
|
||||||
|
);
|
||||||
|
if let Some(token) = page_token {
|
||||||
|
path.push_str(&format!("&page_token={token}"));
|
||||||
|
}
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "kratos list courier")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific courier message.
|
||||||
|
pub async fn get_courier_message(&self, id: &str) -> Result<CourierMessage> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/courier/messages/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos get courier message",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Health -------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Alive health check.
|
||||||
|
pub async fn alive(&self) -> Result<HealthStatus> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"health/alive",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos alive",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ready health check.
|
||||||
|
pub async fn ready(&self) -> Result<HealthStatus> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"health/ready",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"kratos ready",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = KratosClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://id.sunbeam.pt");
|
||||||
|
assert_eq!(c.service_name(), "kratos");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = KratosClient::from_parts(
|
||||||
|
"http://localhost:4434".into(),
|
||||||
|
AuthMethod::None,
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:4434");
|
||||||
|
}
|
||||||
|
}
|
||||||
264
sunbeam-sdk/src/identity/types.rs
Normal file
264
sunbeam-sdk/src/identity/types.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
//! Kratos identity types.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A Kratos identity.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Identity {
|
||||||
|
pub id: String,
|
||||||
|
pub schema_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub schema_url: String,
|
||||||
|
pub traits: serde_json::Value,
|
||||||
|
#[serde(default)]
|
||||||
|
pub state: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub metadata_public: Option<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub metadata_admin: Option<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub verifiable_addresses: Option<Vec<VerifiableAddress>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recovery_addresses: Option<Vec<RecoveryAddress>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub credentials: Option<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub state_changed_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A verifiable address (e.g. email).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VerifiableAddress {
|
||||||
|
pub id: String,
|
||||||
|
pub value: String,
|
||||||
|
pub via: String,
|
||||||
|
pub status: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub verified: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub verified_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A recovery address.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RecoveryAddress {
|
||||||
|
pub id: String,
|
||||||
|
pub value: String,
|
||||||
|
pub via: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for creating an identity.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateIdentityBody {
|
||||||
|
pub schema_id: String,
|
||||||
|
pub traits: serde_json::Value,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metadata_public: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metadata_admin: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub credentials: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub verifiable_addresses: Option<Vec<VerifiableAddress>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub recovery_addresses: Option<Vec<RecoveryAddress>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for updating an identity (PUT).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateIdentityBody {
|
||||||
|
pub schema_id: String,
|
||||||
|
pub traits: serde_json::Value,
|
||||||
|
pub state: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metadata_public: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metadata_admin: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub credentials: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for batch patching identities.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BatchPatchIdentitiesBody {
|
||||||
|
pub identities: Vec<BatchPatchEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single entry in a batch patch.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BatchPatchEntry {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub create: Option<CreateIdentityBody>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub patch_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a batch patch operation.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BatchPatchResult {
|
||||||
|
#[serde(default)]
|
||||||
|
pub identities: Vec<BatchPatchResultEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single entry in batch patch results.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BatchPatchResultEntry {
|
||||||
|
#[serde(default)]
|
||||||
|
pub action: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub identity: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub patch_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Kratos session.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Session {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub active: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub expires_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub authenticated_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub authenticator_assurance_level: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub identity: Option<Identity>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub devices: Option<Vec<SessionDevice>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device info attached to a session.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SessionDevice {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ip_address: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recovery code creation result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RecoveryCodeResult {
|
||||||
|
#[serde(default)]
|
||||||
|
pub recovery_link: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recovery_code: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub expires_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recovery link creation result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RecoveryLinkResult {
|
||||||
|
#[serde(default)]
|
||||||
|
pub recovery_link: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub expires_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An identity schema definition.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IdentitySchema {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub schema: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A courier message.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CourierMessage {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub status: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub r#type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recipient: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Health check response.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct HealthStatus {
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_roundtrip() {
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"id": "abc-123",
|
||||||
|
"schema_id": "employee",
|
||||||
|
"traits": { "email": "test@example.com" },
|
||||||
|
"state": "active"
|
||||||
|
});
|
||||||
|
let identity: Identity = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(identity.id, "abc-123");
|
||||||
|
assert_eq!(identity.schema_id, "employee");
|
||||||
|
assert_eq!(identity.state, Some("active".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_identity_body() {
|
||||||
|
let body = CreateIdentityBody {
|
||||||
|
schema_id: "default".into(),
|
||||||
|
traits: serde_json::json!({"email": "new@example.com"}),
|
||||||
|
state: Some("active".into()),
|
||||||
|
metadata_public: None,
|
||||||
|
metadata_admin: None,
|
||||||
|
credentials: None,
|
||||||
|
verifiable_addresses: None,
|
||||||
|
recovery_addresses: None,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_value(&body).unwrap();
|
||||||
|
assert_eq!(json["schema_id"], "default");
|
||||||
|
assert!(json.get("metadata_public").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_health_status() {
|
||||||
|
let json = serde_json::json!({"status": "ok"});
|
||||||
|
let h: HealthStatus = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(h.status, "ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_recovery_code_result() {
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"recovery_link": "https://example.com/recover",
|
||||||
|
"recovery_code": "abc123"
|
||||||
|
});
|
||||||
|
let r: RecoveryCodeResult = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(r.recovery_code, "abc123");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user