mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-11 12:46:04 +00:00
Admin endoints vs ref
This commit is contained in:
+155
-20
@@ -20,12 +20,39 @@ pub struct GetAccountInfoParams {
|
||||
pub struct AccountInfo {
|
||||
pub did: String,
|
||||
pub handle: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
pub indexed_at: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invite_note: Option<String>,
|
||||
pub invites_disabled: bool,
|
||||
pub email_verified_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email_confirmed_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deactivated_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invited_by: Option<InviteCodeInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invites: Option<Vec<InviteCodeInfo>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InviteCodeInfo {
|
||||
pub code: String,
|
||||
pub available: i32,
|
||||
pub disabled: bool,
|
||||
pub for_account: String,
|
||||
pub created_by: String,
|
||||
pub created_at: String,
|
||||
pub uses: Vec<InviteCodeUseInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InviteCodeUseInfo {
|
||||
pub used_by: String,
|
||||
pub used_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -49,7 +76,7 @@ pub async fn get_account_info(
|
||||
}
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT did, handle, email, created_at
|
||||
SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
|
||||
FROM users
|
||||
WHERE did = $1
|
||||
"#,
|
||||
@@ -58,20 +85,30 @@ pub async fn get_account_info(
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
match result {
|
||||
Ok(Some(row)) => (
|
||||
StatusCode::OK,
|
||||
Json(AccountInfo {
|
||||
did: row.did,
|
||||
handle: row.handle,
|
||||
email: row.email,
|
||||
indexed_at: row.created_at.to_rfc3339(),
|
||||
invite_note: None,
|
||||
invites_disabled: false,
|
||||
email_verified_at: None,
|
||||
deactivated_at: None,
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Ok(Some(row)) => {
|
||||
let invited_by = get_invited_by(&state.db, row.id).await;
|
||||
let invites = get_invites_for_user(&state.db, row.id).await;
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(AccountInfo {
|
||||
did: row.did,
|
||||
handle: row.handle,
|
||||
email: row.email,
|
||||
indexed_at: row.created_at.to_rfc3339(),
|
||||
invite_note: None,
|
||||
invites_disabled: row.invites_disabled.unwrap_or(false),
|
||||
email_confirmed_at: if row.email_verified {
|
||||
Some(row.created_at.to_rfc3339())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
|
||||
invited_by,
|
||||
invites,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Ok(None) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
|
||||
@@ -88,6 +125,96 @@ pub async fn get_account_info(
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_invited_by(
|
||||
db: &sqlx::PgPool,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Option<InviteCodeInfo> {
|
||||
let use_row = sqlx::query!(
|
||||
r#"
|
||||
SELECT icu.code
|
||||
FROM invite_code_uses icu
|
||||
WHERE icu.used_by_user = $1
|
||||
LIMIT 1
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.ok()??;
|
||||
get_invite_code_info(db, &use_row.code).await
|
||||
}
|
||||
|
||||
async fn get_invites_for_user(
|
||||
db: &sqlx::PgPool,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Option<Vec<InviteCodeInfo>> {
|
||||
let codes = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT code FROM invite_codes WHERE created_by_user = $1
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()?;
|
||||
if codes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut invites = Vec::new();
|
||||
for code in codes {
|
||||
if let Some(info) = get_invite_code_info(db, &code).await {
|
||||
invites.push(info);
|
||||
}
|
||||
}
|
||||
if invites.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(invites)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_invite_code_info(db: &sqlx::PgPool, code: &str) -> Option<InviteCodeInfo> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by
|
||||
FROM invite_codes ic
|
||||
JOIN users u ON ic.created_by_user = u.id
|
||||
WHERE ic.code = $1
|
||||
"#,
|
||||
code
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.ok()??;
|
||||
let uses = sqlx::query!(
|
||||
r#"
|
||||
SELECT u.did as used_by, icu.used_at
|
||||
FROM invite_code_uses icu
|
||||
JOIN users u ON icu.used_by_user = u.id
|
||||
WHERE icu.code = $1
|
||||
"#,
|
||||
code
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()?;
|
||||
Some(InviteCodeInfo {
|
||||
code: row.code,
|
||||
available: row.available_uses,
|
||||
disabled: row.disabled.unwrap_or(false),
|
||||
for_account: row.for_account,
|
||||
created_by: row.created_by,
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
uses: uses
|
||||
.into_iter()
|
||||
.map(|u| InviteCodeUseInfo {
|
||||
used_by: u.used_by,
|
||||
used_at: u.used_at.to_rfc3339(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_account_infos(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
@@ -108,7 +235,7 @@ pub async fn get_account_infos(
|
||||
}
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT did, handle, email, created_at
|
||||
SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
|
||||
FROM users
|
||||
WHERE did = $1
|
||||
"#,
|
||||
@@ -117,15 +244,23 @@ pub async fn get_account_infos(
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
if let Ok(Some(row)) = result {
|
||||
let invited_by = get_invited_by(&state.db, row.id).await;
|
||||
let invites = get_invites_for_user(&state.db, row.id).await;
|
||||
infos.push(AccountInfo {
|
||||
did: row.did,
|
||||
handle: row.handle,
|
||||
email: row.email,
|
||||
indexed_at: row.created_at.to_rfc3339(),
|
||||
invite_note: None,
|
||||
invites_disabled: false,
|
||||
email_verified_at: None,
|
||||
deactivated_at: None,
|
||||
invites_disabled: row.invites_disabled.unwrap_or(false),
|
||||
email_confirmed_at: if row.email_verified {
|
||||
Some(row.created_at.to_rfc3339())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
|
||||
invited_by,
|
||||
invites,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use tracing::error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SearchAccountsParams {
|
||||
pub email: Option<String>,
|
||||
pub handle: Option<String>,
|
||||
pub cursor: Option<String>,
|
||||
#[serde(default = "default_limit")]
|
||||
@@ -31,7 +32,7 @@ pub struct AccountView {
|
||||
pub email: Option<String>,
|
||||
pub indexed_at: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email_verified_at: Option<String>,
|
||||
pub email_confirmed_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deactivated_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -53,6 +54,7 @@ pub async fn search_accounts(
|
||||
) -> Response {
|
||||
let limit = params.limit.clamp(1, 100);
|
||||
let cursor_did = params.cursor.as_deref().unwrap_or("");
|
||||
let email_filter = params.email.as_deref().map(|e| format!("%{}%", e));
|
||||
let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
|
||||
let result = sqlx::query_as::<
|
||||
_,
|
||||
@@ -63,17 +65,21 @@ pub async fn search_accounts(
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
bool,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<bool>,
|
||||
),
|
||||
>(
|
||||
r#"
|
||||
SELECT did, handle, email, created_at, email_verified, deactivated_at
|
||||
SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled
|
||||
FROM users
|
||||
WHERE did > $1 AND ($2::text IS NULL OR handle ILIKE $2)
|
||||
WHERE did > $1
|
||||
AND ($2::text IS NULL OR email ILIKE $2)
|
||||
AND ($3::text IS NULL OR handle ILIKE $3)
|
||||
ORDER BY did ASC
|
||||
LIMIT $3
|
||||
LIMIT $4
|
||||
"#,
|
||||
)
|
||||
.bind(cursor_did)
|
||||
.bind(&email_filter)
|
||||
.bind(&handle_filter)
|
||||
.bind(limit + 1)
|
||||
.fetch_all(&state.db)
|
||||
@@ -85,19 +91,19 @@ pub async fn search_accounts(
|
||||
.into_iter()
|
||||
.take(limit as usize)
|
||||
.map(
|
||||
|(did, handle, email, created_at, email_verified, deactivated_at)| {
|
||||
|(did, handle, email, created_at, email_verified, deactivated_at, invites_disabled)| {
|
||||
AccountView {
|
||||
did: did.clone(),
|
||||
handle,
|
||||
email,
|
||||
indexed_at: created_at.to_rfc3339(),
|
||||
email_verified_at: if email_verified {
|
||||
email_confirmed_at: if email_verified {
|
||||
Some(created_at.to_rfc3339())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
deactivated_at: deactivated_at.map(|dt| dt.to_rfc3339()),
|
||||
invites_disabled: None,
|
||||
invites_disabled,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tracing::error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateAccountEmailInput {
|
||||
@@ -128,6 +128,15 @@ pub async fn update_account_handle(
|
||||
let _ = state.cache.delete(&format!("handle:{}", old)).await;
|
||||
}
|
||||
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, did, Some(&handle)).await
|
||||
{
|
||||
warn!("Failed to sequence identity event for admin handle update: {}", e);
|
||||
}
|
||||
if let Err(e) = crate::api::identity::did::update_plc_handle(&state, did, &handle).await
|
||||
{
|
||||
warn!("Failed to update PLC handle for admin handle update: {}", e);
|
||||
}
|
||||
(StatusCode::OK, Json(json!({}))).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
+24
-14
@@ -135,6 +135,16 @@ pub async fn get_subject_status(
|
||||
}
|
||||
}
|
||||
if let Some(blob_cid) = ¶ms.blob {
|
||||
let did = match ¶ms.did {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRequest", "message": "Must provide a did to request blob state"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let blob = sqlx::query!(
|
||||
"SELECT cid, takedown_ref FROM blobs WHERE cid = $1",
|
||||
blob_cid
|
||||
@@ -152,7 +162,7 @@ pub async fn get_subject_status(
|
||||
Json(SubjectStatus {
|
||||
subject: json!({
|
||||
"$type": "com.atproto.admin.defs#repoBlobRef",
|
||||
"did": "",
|
||||
"did": did,
|
||||
"cid": row.cid
|
||||
}),
|
||||
takedown,
|
||||
@@ -195,7 +205,7 @@ pub struct UpdateSubjectStatusInput {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct StatusAttrInput {
|
||||
pub apply: bool,
|
||||
pub applied: bool,
|
||||
pub r#ref: Option<String>,
|
||||
}
|
||||
|
||||
@@ -221,7 +231,7 @@ pub async fn update_subject_status(
|
||||
}
|
||||
};
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let takedown_ref = if takedown.apply {
|
||||
let takedown_ref = if takedown.applied {
|
||||
takedown.r#ref.clone()
|
||||
} else {
|
||||
None
|
||||
@@ -243,7 +253,7 @@ pub async fn update_subject_status(
|
||||
}
|
||||
}
|
||||
if let Some(deactivated) = &input.deactivated {
|
||||
let result = if deactivated.apply {
|
||||
let result = if deactivated.applied {
|
||||
sqlx::query!(
|
||||
"UPDATE users SET deactivated_at = NOW() WHERE did = $1",
|
||||
did
|
||||
@@ -276,7 +286,7 @@ pub async fn update_subject_status(
|
||||
.into_response();
|
||||
}
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let status = if takedown.apply {
|
||||
let status = if takedown.applied {
|
||||
Some("takendown")
|
||||
} else {
|
||||
None
|
||||
@@ -284,7 +294,7 @@ pub async fn update_subject_status(
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
&state,
|
||||
did,
|
||||
!takedown.apply,
|
||||
!takedown.applied,
|
||||
status,
|
||||
)
|
||||
.await
|
||||
@@ -293,7 +303,7 @@ pub async fn update_subject_status(
|
||||
}
|
||||
}
|
||||
if let Some(deactivated) = &input.deactivated {
|
||||
let status = if deactivated.apply {
|
||||
let status = if deactivated.applied {
|
||||
Some("deactivated")
|
||||
} else {
|
||||
None
|
||||
@@ -301,7 +311,7 @@ pub async fn update_subject_status(
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
&state,
|
||||
did,
|
||||
!deactivated.apply,
|
||||
!deactivated.applied,
|
||||
status,
|
||||
)
|
||||
.await
|
||||
@@ -321,11 +331,11 @@ pub async fn update_subject_status(
|
||||
Json(json!({
|
||||
"subject": input.subject,
|
||||
"takedown": input.takedown.as_ref().map(|t| json!({
|
||||
"applied": t.apply,
|
||||
"applied": t.applied,
|
||||
"ref": t.r#ref
|
||||
})),
|
||||
"deactivated": input.deactivated.as_ref().map(|d| json!({
|
||||
"applied": d.apply
|
||||
"applied": d.applied
|
||||
}))
|
||||
})),
|
||||
)
|
||||
@@ -336,7 +346,7 @@ pub async fn update_subject_status(
|
||||
let uri = input.subject.get("uri").and_then(|u| u.as_str());
|
||||
if let Some(uri) = uri {
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let takedown_ref = if takedown.apply {
|
||||
let takedown_ref = if takedown.applied {
|
||||
takedown.r#ref.clone()
|
||||
} else {
|
||||
None
|
||||
@@ -365,7 +375,7 @@ pub async fn update_subject_status(
|
||||
Json(json!({
|
||||
"subject": input.subject,
|
||||
"takedown": input.takedown.as_ref().map(|t| json!({
|
||||
"applied": t.apply,
|
||||
"applied": t.applied,
|
||||
"ref": t.r#ref
|
||||
}))
|
||||
})),
|
||||
@@ -377,7 +387,7 @@ pub async fn update_subject_status(
|
||||
let cid = input.subject.get("cid").and_then(|c| c.as_str());
|
||||
if let Some(cid) = cid {
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let takedown_ref = if takedown.apply {
|
||||
let takedown_ref = if takedown.applied {
|
||||
takedown.r#ref.clone()
|
||||
} else {
|
||||
None
|
||||
@@ -403,7 +413,7 @@ pub async fn update_subject_status(
|
||||
Json(json!({
|
||||
"subject": input.subject,
|
||||
"takedown": input.takedown.as_ref().map(|t| json!({
|
||||
"applied": t.apply,
|
||||
"applied": t.applied,
|
||||
"ref": t.r#ref
|
||||
}))
|
||||
})),
|
||||
|
||||
@@ -780,7 +780,7 @@ pub async fn update_handle(
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_plc_handle(
|
||||
pub async fn update_plc_handle(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
new_handle: &str,
|
||||
|
||||
Reference in New Issue
Block a user