mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-02-08 21:30:08 +00:00
Session conf. vs ref
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n u.id, u.did, u.handle, u.password_hash,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login,\n u.preferred_comms_channel as \"preferred_comms_channel: crate::comms::CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1",
|
||||
"query": "SELECT\n u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login,\n u.preferred_comms_channel as \"preferred_comms_channel: crate::comms::CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,31 +25,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "takedown_ref",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "email_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 8,
|
||||
"name": "discord_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 9,
|
||||
"name": "telegram_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 10,
|
||||
"name": "signal_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 11,
|
||||
"name": "allow_legacy_login",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 12,
|
||||
"name": "preferred_comms_channel: crate::comms::CommsChannel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -66,17 +81,17 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 13,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 14,
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 15,
|
||||
"name": "totp_enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -91,6 +106,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -102,5 +120,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fe8f204d593dce319bb4624871a3a597ba1d3d9ea32855704b18948fd6bbae38"
|
||||
"hash": "1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
|
||||
"query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at, takedown_ref, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,11 +30,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "takedown_ref",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "preferred_locale",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "preferred_channel: crate::comms::CommsChannel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -51,17 +56,17 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "discord_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "telegram_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "signal_verified",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -78,11 +83,12 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd"
|
||||
"hash": "c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n handle, email, email_verified, is_admin, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
|
||||
"query": "SELECT\n handle, email, email_verified, is_admin, preferred_locale, deactivated_at, takedown_ref,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,6 +30,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "takedown_ref",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "preferred_channel: crate::comms::CommsChannel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -46,17 +56,17 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 8,
|
||||
"name": "discord_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 9,
|
||||
"name": "telegram_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 10,
|
||||
"name": "signal_verified",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -72,11 +82,13 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c"
|
||||
"hash": "d4e4c9de4330cc017f457eaec4195b0cf35607d2d0ef6b73e9bb5e94e7742e7a"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT handle FROM users WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e223898d53602c1c8b23eb08a4b96cf20ac349d1fa4e91334b225d3069209dcf"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, handle, deactivated_at FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e60550cc972a5b0dd7cbdbc20d6ae6439eae3811d488166dca1b41bcc11f81f7"
|
||||
}
|
||||
@@ -43,9 +43,12 @@ fn full_handle(stored_handle: &str, _pds_hostname: &str) -> String {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateSessionInput {
|
||||
pub identifier: String,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub allow_takendown: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -55,6 +58,16 @@ pub struct CreateSessionOutput {
|
||||
pub refresh_jwt: String,
|
||||
pub handle: String,
|
||||
pub did: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub did_doc: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email_confirmed: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_session(
|
||||
@@ -89,7 +102,7 @@ pub async fn create_session(
|
||||
);
|
||||
let row = match sqlx::query!(
|
||||
r#"SELECT
|
||||
u.id, u.did, u.handle, u.password_hash,
|
||||
u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,
|
||||
u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,
|
||||
u.allow_legacy_login,
|
||||
u.preferred_comms_channel as "preferred_comms_channel: crate::comms::CommsChannel",
|
||||
@@ -157,6 +170,18 @@ pub async fn create_session(
|
||||
return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into())
|
||||
.into_response();
|
||||
}
|
||||
let is_takendown = row.takedown_ref.is_some();
|
||||
if is_takendown && !input.allow_takendown {
|
||||
warn!("Login attempt for takendown account: {}", row.did);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "AccountTakedown",
|
||||
"message": "Account has been taken down"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let is_verified =
|
||||
row.email_verified || row.discord_verified || row.telegram_verified || row.signal_verified;
|
||||
let is_delegated = crate::delegation::is_delegated_account(&state.db, &row.did)
|
||||
@@ -207,21 +232,25 @@ pub async fn create_session(
|
||||
return ApiError::InternalError.into_response();
|
||||
}
|
||||
};
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified, scope, controller_did) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||
row.did,
|
||||
access_meta.jti,
|
||||
refresh_meta.jti,
|
||||
access_meta.expires_at,
|
||||
refresh_meta.expires_at,
|
||||
is_legacy_login,
|
||||
false,
|
||||
app_password_scopes,
|
||||
app_password_controller
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
{
|
||||
let did_for_doc = row.did.clone();
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
let (insert_result, did_doc) = tokio::join!(
|
||||
sqlx::query!(
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified, scope, controller_did) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||
row.did,
|
||||
access_meta.jti,
|
||||
refresh_meta.jti,
|
||||
access_meta.expires_at,
|
||||
refresh_meta.expires_at,
|
||||
is_legacy_login,
|
||||
false,
|
||||
app_password_scopes,
|
||||
app_password_controller
|
||||
)
|
||||
.execute(&state.db),
|
||||
did_resolver.resolve_did_document(&did_for_doc)
|
||||
);
|
||||
if let Err(e) = insert_result {
|
||||
error!("Failed to insert session: {:?}", e);
|
||||
return ApiError::InternalError.into_response();
|
||||
}
|
||||
@@ -245,11 +274,24 @@ pub async fn create_session(
|
||||
}
|
||||
}
|
||||
let handle = full_handle(&row.handle, &pds_hostname);
|
||||
let is_active = row.deactivated_at.is_none() && !is_takendown;
|
||||
let status = if is_takendown {
|
||||
Some("takendown".to_string())
|
||||
} else if row.deactivated_at.is_some() {
|
||||
Some("deactivated".to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Json(CreateSessionOutput {
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
handle,
|
||||
did: row.did,
|
||||
did_doc,
|
||||
email: row.email,
|
||||
email_confirmed: Some(row.email_verified),
|
||||
active: Some(is_active),
|
||||
status,
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
@@ -261,17 +303,21 @@ pub async fn get_session(
|
||||
let permissions = auth_user.permissions();
|
||||
let can_read_email = permissions.allows_email_read();
|
||||
|
||||
match sqlx::query!(
|
||||
r#"SELECT
|
||||
handle, email, email_verified, is_admin, deactivated_at, preferred_locale,
|
||||
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
|
||||
discord_verified, telegram_verified, signal_verified
|
||||
FROM users WHERE did = $1"#,
|
||||
auth_user.did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
let did_for_doc = auth_user.did.clone();
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
let (db_result, did_doc) = tokio::join!(
|
||||
sqlx::query!(
|
||||
r#"SELECT
|
||||
handle, email, email_verified, is_admin, deactivated_at, takedown_ref, preferred_locale,
|
||||
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
|
||||
discord_verified, telegram_verified, signal_verified
|
||||
FROM users WHERE did = $1"#,
|
||||
auth_user.did
|
||||
)
|
||||
.fetch_optional(&state.db),
|
||||
did_resolver.resolve_did_document(&did_for_doc)
|
||||
);
|
||||
match db_result {
|
||||
Ok(Some(row)) => {
|
||||
let (preferred_channel, preferred_channel_verified) = match row.preferred_channel {
|
||||
crate::comms::CommsChannel::Email => ("email", row.email_verified),
|
||||
@@ -282,26 +328,36 @@ pub async fn get_session(
|
||||
let pds_hostname =
|
||||
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let handle = full_handle(&row.handle, &pds_hostname);
|
||||
let is_active = row.deactivated_at.is_none();
|
||||
let is_takendown = row.takedown_ref.is_some();
|
||||
let is_active = row.deactivated_at.is_none() && !is_takendown;
|
||||
let email_value = if can_read_email {
|
||||
row.email.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let email_verified_value = can_read_email && row.email_verified;
|
||||
Json(json!({
|
||||
let email_confirmed_value = can_read_email && row.email_verified;
|
||||
let mut response = json!({
|
||||
"handle": handle,
|
||||
"did": auth_user.did,
|
||||
"email": email_value,
|
||||
"emailVerified": email_verified_value,
|
||||
"active": is_active,
|
||||
"preferredChannel": preferred_channel,
|
||||
"preferredChannelVerified": preferred_channel_verified,
|
||||
"preferredLocale": row.preferred_locale,
|
||||
"isAdmin": row.is_admin,
|
||||
"active": is_active,
|
||||
"status": if is_active { "active" } else { "deactivated" },
|
||||
"didDoc": {}
|
||||
}))
|
||||
"isAdmin": row.is_admin
|
||||
});
|
||||
if can_read_email {
|
||||
response["email"] = json!(email_value);
|
||||
response["emailConfirmed"] = json!(email_confirmed_value);
|
||||
}
|
||||
if is_takendown {
|
||||
response["status"] = json!("takendown");
|
||||
} else if row.deactivated_at.is_some() {
|
||||
response["status"] = json!("deactivated");
|
||||
}
|
||||
if let Some(doc) = did_doc {
|
||||
response["didDoc"] = doc;
|
||||
}
|
||||
Json(response)
|
||||
.into_response()
|
||||
}
|
||||
Ok(None) => ApiError::AuthenticationFailed.into_response(),
|
||||
@@ -498,17 +554,21 @@ pub async fn refresh_session(
|
||||
error!("Failed to commit transaction: {:?}", e);
|
||||
return ApiError::InternalError.into_response();
|
||||
}
|
||||
match sqlx::query!(
|
||||
r#"SELECT
|
||||
handle, email, email_verified, is_admin, preferred_locale,
|
||||
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
|
||||
discord_verified, telegram_verified, signal_verified
|
||||
FROM users WHERE did = $1"#,
|
||||
session_row.did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
let did_for_doc = session_row.did.clone();
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
let (db_result, did_doc) = tokio::join!(
|
||||
sqlx::query!(
|
||||
r#"SELECT
|
||||
handle, email, email_verified, is_admin, preferred_locale, deactivated_at, takedown_ref,
|
||||
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
|
||||
discord_verified, telegram_verified, signal_verified
|
||||
FROM users WHERE did = $1"#,
|
||||
session_row.did
|
||||
)
|
||||
.fetch_optional(&state.db),
|
||||
did_resolver.resolve_did_document(&did_for_doc)
|
||||
);
|
||||
match db_result {
|
||||
Ok(Some(u)) => {
|
||||
let (preferred_channel, preferred_channel_verified) = match u.preferred_channel {
|
||||
crate::comms::CommsChannel::Email => ("email", u.email_verified),
|
||||
@@ -519,19 +579,30 @@ pub async fn refresh_session(
|
||||
let pds_hostname =
|
||||
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let handle = full_handle(&u.handle, &pds_hostname);
|
||||
Json(json!({
|
||||
let is_takendown = u.takedown_ref.is_some();
|
||||
let is_active = u.deactivated_at.is_none() && !is_takendown;
|
||||
let mut response = json!({
|
||||
"accessJwt": new_access_meta.token,
|
||||
"refreshJwt": new_refresh_meta.token,
|
||||
"handle": handle,
|
||||
"did": session_row.did,
|
||||
"email": u.email,
|
||||
"emailVerified": u.email_verified,
|
||||
"emailConfirmed": u.email_verified,
|
||||
"preferredChannel": preferred_channel,
|
||||
"preferredChannelVerified": preferred_channel_verified,
|
||||
"preferredLocale": u.preferred_locale,
|
||||
"isAdmin": u.is_admin,
|
||||
"active": true
|
||||
}))
|
||||
"active": is_active
|
||||
});
|
||||
if let Some(doc) = did_doc {
|
||||
response["didDoc"] = doc;
|
||||
}
|
||||
if is_takendown {
|
||||
response["status"] = json!("takendown");
|
||||
} else if u.deactivated_at.is_some() {
|
||||
response["status"] = json!("deactivated");
|
||||
}
|
||||
Json(response)
|
||||
.into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
|
||||
@@ -29,6 +29,12 @@ struct CachedDid {
|
||||
resolved_at: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedDidDocument {
|
||||
document: serde_json::Value,
|
||||
resolved_at: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedService {
|
||||
pub url: String,
|
||||
@@ -37,6 +43,7 @@ pub struct ResolvedService {
|
||||
|
||||
pub struct DidResolver {
|
||||
did_cache: RwLock<HashMap<String, CachedDid>>,
|
||||
did_doc_cache: RwLock<HashMap<String, CachedDidDocument>>,
|
||||
client: Client,
|
||||
cache_ttl: Duration,
|
||||
plc_directory_url: String,
|
||||
@@ -46,6 +53,7 @@ impl Clone for DidResolver {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
did_cache: RwLock::new(HashMap::new()),
|
||||
did_doc_cache: RwLock::new(HashMap::new()),
|
||||
client: self.client.clone(),
|
||||
cache_ttl: self.cache_ttl,
|
||||
plc_directory_url: self.plc_directory_url.clone(),
|
||||
@@ -74,12 +82,60 @@ impl DidResolver {
|
||||
|
||||
Self {
|
||||
did_cache: RwLock::new(HashMap::new()),
|
||||
did_doc_cache: RwLock::new(HashMap::new()),
|
||||
client,
|
||||
cache_ttl: Duration::from_secs(cache_ttl_secs),
|
||||
plc_directory_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_did_web_url(did: &str) -> Result<String, String> {
|
||||
let host = did
|
||||
.strip_prefix("did:web:")
|
||||
.ok_or("Invalid did:web format")?;
|
||||
|
||||
let (host, path) = if host.contains(':') {
|
||||
let decoded = host.replace("%3A", ":");
|
||||
let parts: Vec<&str> = decoded.splitn(2, '/').collect();
|
||||
if parts.len() > 1 {
|
||||
(parts[0].to_string(), format!("/{}", parts[1]))
|
||||
} else {
|
||||
(decoded, String::new())
|
||||
}
|
||||
} else {
|
||||
let parts: Vec<&str> = host.splitn(2, ':').collect();
|
||||
if parts.len() > 1 && parts[1].contains('/') {
|
||||
let path_parts: Vec<&str> = parts[1].splitn(2, '/').collect();
|
||||
if path_parts.len() > 1 {
|
||||
(
|
||||
format!("{}:{}", parts[0], path_parts[0]),
|
||||
format!("/{}", path_parts[1]),
|
||||
)
|
||||
} else {
|
||||
(host.to_string(), String::new())
|
||||
}
|
||||
} else {
|
||||
(host.to_string(), String::new())
|
||||
}
|
||||
};
|
||||
|
||||
let scheme =
|
||||
if host.starts_with("localhost") || host.starts_with("127.0.0.1") || host.contains(':')
|
||||
{
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
|
||||
let url = if path.is_empty() {
|
||||
format!("{}://{}/.well-known/did.json", scheme, host)
|
||||
} else {
|
||||
format!("{}://{}{}/did.json", scheme, host, path)
|
||||
};
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
pub async fn resolve_did(&self, did: &str) -> Option<ResolvedService> {
|
||||
{
|
||||
let cache = self.did_cache.read().await;
|
||||
@@ -140,48 +196,7 @@ impl DidResolver {
|
||||
}
|
||||
|
||||
async fn resolve_did_web(&self, did: &str) -> Result<DidDocument, String> {
|
||||
let host = did
|
||||
.strip_prefix("did:web:")
|
||||
.ok_or("Invalid did:web format")?;
|
||||
|
||||
let (host, path) = if host.contains(':') {
|
||||
let decoded = host.replace("%3A", ":");
|
||||
let parts: Vec<&str> = decoded.splitn(2, '/').collect();
|
||||
if parts.len() > 1 {
|
||||
(parts[0].to_string(), format!("/{}", parts[1]))
|
||||
} else {
|
||||
(decoded, String::new())
|
||||
}
|
||||
} else {
|
||||
let parts: Vec<&str> = host.splitn(2, ':').collect();
|
||||
if parts.len() > 1 && parts[1].contains('/') {
|
||||
let path_parts: Vec<&str> = parts[1].splitn(2, '/').collect();
|
||||
if path_parts.len() > 1 {
|
||||
(
|
||||
format!("{}:{}", parts[0], path_parts[0]),
|
||||
format!("/{}", path_parts[1]),
|
||||
)
|
||||
} else {
|
||||
(host.to_string(), String::new())
|
||||
}
|
||||
} else {
|
||||
(host.to_string(), String::new())
|
||||
}
|
||||
};
|
||||
|
||||
let scheme =
|
||||
if host.starts_with("localhost") || host.starts_with("127.0.0.1") || host.contains(':')
|
||||
{
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
|
||||
let url = if path.is_empty() {
|
||||
format!("{}://{}/.well-known/did.json", scheme, host)
|
||||
} else {
|
||||
format!("{}://{}{}/did.json", scheme, host, path)
|
||||
};
|
||||
let url = Self::build_did_web_url(did)?;
|
||||
|
||||
debug!("Resolving did:web {} via {}", did, url);
|
||||
|
||||
@@ -286,9 +301,92 @@ impl DidResolver {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn resolve_did_document(&self, did: &str) -> Option<serde_json::Value> {
|
||||
{
|
||||
let cache = self.did_doc_cache.read().await;
|
||||
if let Some(cached) = cache.get(did)
|
||||
&& cached.resolved_at.elapsed() < self.cache_ttl
|
||||
{
|
||||
return Some(cached.document.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let result = if did.starts_with("did:web:") {
|
||||
self.fetch_did_document_web(did).await
|
||||
} else if did.starts_with("did:plc:") {
|
||||
self.fetch_did_document_plc(did).await
|
||||
} else {
|
||||
warn!("Unsupported DID method for document resolution: {}", did);
|
||||
return None;
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(doc) => {
|
||||
let mut cache = self.did_doc_cache.write().await;
|
||||
cache.insert(
|
||||
did.to_string(),
|
||||
CachedDidDocument {
|
||||
document: doc.clone(),
|
||||
resolved_at: Instant::now(),
|
||||
},
|
||||
);
|
||||
Some(doc)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to resolve DID document for {}: {}", did, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_did_document_web(&self, did: &str) -> Result<serde_json::Value, String> {
|
||||
let url = Self::build_did_web_url(did)?;
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("HTTP {}", resp.status()));
|
||||
}
|
||||
|
||||
resp.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse DID document: {}", e))
|
||||
}
|
||||
|
||||
async fn fetch_did_document_plc(&self, did: &str) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did));
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
||||
|
||||
if resp.status() == reqwest::StatusCode::NOT_FOUND {
|
||||
return Err("DID not found".to_string());
|
||||
}
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("HTTP {}", resp.status()));
|
||||
}
|
||||
|
||||
resp.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse DID document: {}", e))
|
||||
}
|
||||
|
||||
pub async fn invalidate_cache(&self, did: &str) {
|
||||
let mut cache = self.did_cache.write().await;
|
||||
cache.remove(did);
|
||||
drop(cache);
|
||||
let mut doc_cache = self.did_doc_cache.write().await;
|
||||
doc_cache.remove(did);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user