Creating & posting records works. Also messed up newlines but will fix later.

This commit is contained in:
lewis
2025-12-14 23:55:04 +02:00
parent 86db6617af
commit c6f9062979
227 changed files with 2122 additions and 7842 deletions
+4 -20
View File
@@ -7,13 +7,11 @@ use axum::{
};
use serde::Deserialize;
use serde_json::json;
use tracing::error;
use tracing::{error, warn};
#[derive(Deserialize)]
pub struct DeleteAccountInput {
pub did: String,
}
pub async fn delete_account(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -27,7 +25,6 @@ pub async fn delete_account(
)
.into_response();
}
let did = input.did.trim();
if did.is_empty() {
return (
@@ -36,11 +33,9 @@ pub async fn delete_account(
)
.into_response();
}
let user = sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await;
let (user_id, handle) = match user {
Ok(Some(row)) => (row.id, row.handle),
Ok(None) => {
@@ -59,7 +54,6 @@ pub async fn delete_account(
.into_response();
}
};
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
@@ -71,7 +65,6 @@ pub async fn delete_account(
.into_response();
}
};
if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did)
.execute(&mut *tx)
.await
@@ -83,14 +76,12 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = sqlx::query!("DELETE FROM used_refresh_tokens WHERE session_id IN (SELECT id FROM session_tokens WHERE did = $1)", did)
.execute(&mut *tx)
.await
{
error!("Failed to delete used refresh tokens for {}: {:?}", did, e);
}
if let Err(e) = sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
.execute(&mut *tx)
.await
@@ -102,7 +93,6 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await
@@ -114,7 +104,6 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
.execute(&mut *tx)
.await
@@ -126,7 +115,6 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await
@@ -138,21 +126,18 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = sqlx::query!("DELETE FROM invite_code_uses WHERE used_by_user = $1", user_id)
.execute(&mut *tx)
.await
{
error!("Failed to delete invite code uses for user {}: {:?}", user_id, e);
}
if let Err(e) = sqlx::query!("DELETE FROM invite_codes WHERE created_by_user = $1", user_id)
.execute(&mut *tx)
.await
{
error!("Failed to delete invite codes for user {}: {:?}", user_id, e);
}
if let Err(e) = sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await
@@ -164,7 +149,6 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
.execute(&mut *tx)
.await
@@ -176,7 +160,6 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = tx.commit().await {
error!("Failed to commit account deletion transaction: {:?}", e);
return (
@@ -185,8 +168,9 @@ pub async fn delete_account(
)
.into_response();
}
if let Err(e) = crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await {
warn!("Failed to sequence account deletion event for {}: {}", did, e);
}
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
(StatusCode::OK, Json(json!({}))).into_response()
}
-12
View File
@@ -8,7 +8,6 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, warn};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendEmailInput {
@@ -18,12 +17,10 @@ pub struct SendEmailInput {
pub subject: Option<String>,
pub comment: Option<String>,
}
#[derive(Serialize)]
pub struct SendEmailOutput {
pub sent: bool,
}
pub async fn send_email(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -37,10 +34,8 @@ pub async fn send_email(
)
.into_response();
}
let recipient_did = input.recipient_did.trim();
let content = input.content.trim();
if recipient_did.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -48,7 +43,6 @@ pub async fn send_email(
)
.into_response();
}
if content.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -56,14 +50,12 @@ pub async fn send_email(
)
.into_response();
}
let user = sqlx::query!(
"SELECT id, email, handle FROM users WHERE did = $1",
recipient_did
)
.fetch_optional(&state.db)
.await;
let (user_id, email, handle) = match user {
Ok(Some(row)) => {
let email = match row.email {
@@ -94,13 +86,11 @@ pub async fn send_email(
.into_response();
}
};
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
let subject = input
.subject
.clone()
.unwrap_or_else(|| format!("Message from {}", hostname));
let notification = crate::notifications::NewNotification::email(
user_id,
crate::notifications::NotificationType::AdminEmail,
@@ -108,9 +98,7 @@ pub async fn send_email(
subject,
content.to_string(),
);
let result = crate::notifications::enqueue_notification(&state.db, notification).await;
match result {
Ok(_) => {
tracing::info!(
-15
View File
@@ -8,12 +8,10 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::error;
#[derive(Deserialize)]
pub struct GetAccountInfoParams {
pub did: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountInfo {
@@ -26,13 +24,11 @@ pub struct AccountInfo {
pub email_confirmed_at: Option<String>,
pub deactivated_at: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetAccountInfosOutput {
pub infos: Vec<AccountInfo>,
}
pub async fn get_account_info(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -46,7 +42,6 @@ pub async fn get_account_info(
)
.into_response();
}
let did = params.did.trim();
if did.is_empty() {
return (
@@ -55,7 +50,6 @@ pub async fn get_account_info(
)
.into_response();
}
let result = sqlx::query!(
r#"
SELECT did, handle, email, created_at
@@ -66,7 +60,6 @@ pub async fn get_account_info(
)
.fetch_optional(&state.db)
.await;
match result {
Ok(Some(row)) => {
(
@@ -99,12 +92,10 @@ pub async fn get_account_info(
}
}
}
#[derive(Deserialize)]
pub struct GetAccountInfosParams {
pub dids: String,
}
pub async fn get_account_infos(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -118,7 +109,6 @@ pub async fn get_account_infos(
)
.into_response();
}
let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect();
if dids.is_empty() {
return (
@@ -127,14 +117,11 @@ pub async fn get_account_infos(
)
.into_response();
}
let mut infos = Vec::new();
for did in dids {
if did.is_empty() {
continue;
}
let result = sqlx::query!(
r#"
SELECT did, handle, email, created_at
@@ -145,7 +132,6 @@ pub async fn get_account_infos(
)
.fetch_optional(&state.db)
.await;
if let Ok(Some(row)) = result {
infos.push(AccountInfo {
did: row.did,
@@ -159,6 +145,5 @@ pub async fn get_account_infos(
});
}
}
(StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
}
+2 -1
View File
@@ -1,14 +1,15 @@
mod delete;
mod email;
mod info;
mod profile;
mod update;
pub use delete::{delete_account, DeleteAccountInput};
pub use email::{send_email, SendEmailInput, SendEmailOutput};
pub use info::{
get_account_info, get_account_infos, AccountInfo, GetAccountInfoParams, GetAccountInfosOutput,
GetAccountInfosParams,
};
pub use profile::{create_profile, create_record_admin, CreateProfileInput, CreateProfileOutput, CreateRecordAdminInput};
pub use update::{
update_account_email, update_account_handle, update_account_password, UpdateAccountEmailInput,
UpdateAccountHandleInput, UpdateAccountPasswordInput,
+154
View File
@@ -0,0 +1,154 @@
use crate::api::repo::record::create_record_internal;
use crate::state::AppState;
use axum::{
Json,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, info};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateProfileInput {
pub did: String,
pub display_name: Option<String>,
pub description: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateRecordAdminInput {
pub did: String,
pub collection: String,
pub rkey: Option<String>,
pub record: serde_json::Value,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateProfileOutput {
pub uri: String,
pub cid: String,
}
pub async fn create_profile(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
Json(input): Json<CreateProfileInput>,
) -> Response {
let auth_header = headers.get("Authorization");
if auth_header.is_none() {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
let did = input.did.trim();
if did.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
)
.into_response();
}
let mut profile_record = json!({
"$type": "app.bsky.actor.profile"
});
if let Some(display_name) = &input.display_name {
profile_record["displayName"] = json!(display_name);
}
if let Some(description) = &input.description {
profile_record["description"] = json!(description);
}
match create_record_internal(
&state,
did,
"app.bsky.actor.profile",
"self",
&profile_record,
).await {
Ok((uri, commit_cid)) => {
info!(did = %did, uri = %uri, "Created profile for user");
(
StatusCode::OK,
Json(CreateProfileOutput {
uri,
cid: commit_cid.to_string(),
}),
)
.into_response()
}
Err(e) => {
error!("Failed to create profile for {}: {}", did, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError", "message": e})),
)
.into_response()
}
}
}
pub async fn create_record_admin(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
Json(input): Json<CreateRecordAdminInput>,
) -> Response {
let auth_header = headers.get("Authorization");
if auth_header.is_none() {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
let did = input.did.trim();
if did.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
)
.into_response();
}
let rkey = input.rkey.unwrap_or_else(|| {
chrono::Utc::now().format("%Y%m%d%H%M%S%f").to_string()
});
match create_record_internal(
&state,
did,
&input.collection,
&rkey,
&input.record,
).await {
Ok((uri, commit_cid)) => {
info!(did = %did, uri = %uri, "Admin created record");
(
StatusCode::OK,
Json(CreateProfileOutput {
uri,
cid: commit_cid.to_string(),
}),
)
.into_response()
}
Err(e) => {
error!("Failed to create record for {}: {}", did, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError", "message": e})),
)
.into_response()
}
}
}
-23
View File
@@ -8,13 +8,11 @@ use axum::{
use serde::Deserialize;
use serde_json::json;
use tracing::error;
#[derive(Deserialize)]
pub struct UpdateAccountEmailInput {
pub account: String,
pub email: String,
}
pub async fn update_account_email(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -28,10 +26,8 @@ pub async fn update_account_email(
)
.into_response();
}
let account = input.account.trim();
let email = input.email.trim();
if account.is_empty() || email.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -39,11 +35,9 @@ pub async fn update_account_email(
)
.into_response();
}
let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account)
.execute(&state.db)
.await;
match result {
Ok(r) => {
if r.rows_affected() == 0 {
@@ -65,13 +59,11 @@ pub async fn update_account_email(
}
}
}
#[derive(Deserialize)]
pub struct UpdateAccountHandleInput {
pub did: String,
pub handle: String,
}
pub async fn update_account_handle(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -85,10 +77,8 @@ pub async fn update_account_handle(
)
.into_response();
}
let did = input.did.trim();
let handle = input.handle.trim();
if did.is_empty() || handle.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -96,7 +86,6 @@ pub async fn update_account_handle(
)
.into_response();
}
if !handle
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
@@ -107,17 +96,14 @@ pub async fn update_account_handle(
)
.into_response();
}
let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
.ok()
.flatten();
let existing = sqlx::query!("SELECT id FROM users WHERE handle = $1 AND did != $2", handle, did)
.fetch_optional(&state.db)
.await;
if let Ok(Some(_)) = existing {
return (
StatusCode::BAD_REQUEST,
@@ -125,11 +111,9 @@ pub async fn update_account_handle(
)
.into_response();
}
let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did)
.execute(&state.db)
.await;
match result {
Ok(r) => {
if r.rows_affected() == 0 {
@@ -155,13 +139,11 @@ pub async fn update_account_handle(
}
}
}
#[derive(Deserialize)]
pub struct UpdateAccountPasswordInput {
pub did: String,
pub password: String,
}
pub async fn update_account_password(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -175,10 +157,8 @@ pub async fn update_account_password(
)
.into_response();
}
let did = input.did.trim();
let password = input.password.trim();
if did.is_empty() || password.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -186,7 +166,6 @@ pub async fn update_account_password(
)
.into_response();
}
let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) {
Ok(h) => h,
Err(e) => {
@@ -198,11 +177,9 @@ pub async fn update_account_password(
.into_response();
}
};
let result = sqlx::query!("UPDATE users SET password_hash = $1 WHERE did = $2", password_hash, did)
.execute(&state.db)
.await;
match result {
Ok(r) => {
if r.rows_affected() == 0 {
-31
View File
@@ -8,14 +8,12 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::error;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DisableInviteCodesInput {
pub codes: Option<Vec<String>>,
pub accounts: Option<Vec<String>>,
}
pub async fn disable_invite_codes(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -29,7 +27,6 @@ pub async fn disable_invite_codes(
)
.into_response();
}
if let Some(codes) = &input.codes {
for code in codes {
let _ = sqlx::query!("UPDATE invite_codes SET disabled = TRUE WHERE code = $1", code)
@@ -37,13 +34,11 @@ pub async fn disable_invite_codes(
.await;
}
}
if let Some(accounts) = &input.accounts {
for account in accounts {
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account)
.fetch_optional(&state.db)
.await;
if let Ok(Some(user_row)) = user {
let _ = sqlx::query!(
"UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1",
@@ -54,17 +49,14 @@ pub async fn disable_invite_codes(
}
}
}
(StatusCode::OK, Json(json!({}))).into_response()
}
#[derive(Deserialize)]
pub struct GetInviteCodesParams {
pub sort: Option<String>,
pub limit: Option<i64>,
pub cursor: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InviteCodeInfo {
@@ -76,20 +68,17 @@ pub struct InviteCodeInfo {
pub created_at: String,
pub uses: Vec<InviteCodeUseInfo>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InviteCodeUseInfo {
pub used_by: String,
pub used_at: String,
}
#[derive(Serialize)]
pub struct GetInviteCodesOutput {
pub cursor: Option<String>,
pub codes: Vec<InviteCodeInfo>,
}
pub async fn get_invite_codes(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -103,15 +92,12 @@ pub async fn get_invite_codes(
)
.into_response();
}
let limit = params.limit.unwrap_or(100).clamp(1, 500);
let sort = params.sort.as_deref().unwrap_or("recent");
let order_clause = match sort {
"usage" => "available_uses DESC",
_ => "created_at DESC",
};
let codes_result = if let Some(cursor) = &params.cursor {
sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!(
r#"
@@ -141,7 +127,6 @@ pub async fn get_invite_codes(
.fetch_all(&state.db)
.await
};
let codes_rows = match codes_result {
Ok(rows) => rows,
Err(e) => {
@@ -153,7 +138,6 @@ pub async fn get_invite_codes(
.into_response();
}
};
let mut codes = Vec::new();
for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows {
let creator_did = sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user)
@@ -162,7 +146,6 @@ pub async fn get_invite_codes(
.ok()
.flatten()
.unwrap_or_else(|| "unknown".to_string());
let uses_result = sqlx::query!(
r#"
SELECT u.did, icu.used_at
@@ -175,7 +158,6 @@ pub async fn get_invite_codes(
)
.fetch_all(&state.db)
.await;
let uses = match uses_result {
Ok(use_rows) => use_rows
.iter()
@@ -186,7 +168,6 @@ pub async fn get_invite_codes(
.collect(),
Err(_) => Vec::new(),
};
codes.push(InviteCodeInfo {
code: code.clone(),
available: *available_uses,
@@ -197,13 +178,11 @@ pub async fn get_invite_codes(
uses,
});
}
let next_cursor = if codes_rows.len() == limit as usize {
codes_rows.last().map(|(code, _, _, _, _)| code.clone())
} else {
None
};
(
StatusCode::OK,
Json(GetInviteCodesOutput {
@@ -213,12 +192,10 @@ pub async fn get_invite_codes(
)
.into_response()
}
#[derive(Deserialize)]
pub struct DisableAccountInvitesInput {
pub account: String,
}
pub async fn disable_account_invites(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -232,7 +209,6 @@ pub async fn disable_account_invites(
)
.into_response();
}
let account = input.account.trim();
if account.is_empty() {
return (
@@ -241,11 +217,9 @@ pub async fn disable_account_invites(
)
.into_response();
}
let result = sqlx::query!("UPDATE users SET invites_disabled = TRUE WHERE did = $1", account)
.execute(&state.db)
.await;
match result {
Ok(r) => {
if r.rows_affected() == 0 {
@@ -267,12 +241,10 @@ pub async fn disable_account_invites(
}
}
}
#[derive(Deserialize)]
pub struct EnableAccountInvitesInput {
pub account: String,
}
pub async fn enable_account_invites(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -286,7 +258,6 @@ pub async fn enable_account_invites(
)
.into_response();
}
let account = input.account.trim();
if account.is_empty() {
return (
@@ -295,11 +266,9 @@ pub async fn enable_account_invites(
)
.into_response();
}
let result = sqlx::query!("UPDATE users SET invites_disabled = FALSE WHERE did = $1", account)
.execute(&state.db)
.await;
match result {
Ok(r) => {
if r.rows_affected() == 0 {
+2 -3
View File
@@ -1,10 +1,9 @@
pub mod account;
pub mod invite;
pub mod status;
pub use account::{
delete_account, get_account_info, get_account_infos, send_email, update_account_email,
update_account_handle, update_account_password,
create_profile, create_record_admin, delete_account, get_account_info, get_account_infos,
send_email, update_account_email, update_account_handle, update_account_password,
};
pub use invite::{
disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes,
+13 -30
View File
@@ -7,29 +7,25 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::error;
use tracing::{error, warn};
#[derive(Deserialize)]
pub struct GetSubjectStatusParams {
pub did: Option<String>,
pub uri: Option<String>,
pub blob: Option<String>,
}
#[derive(Serialize)]
pub struct SubjectStatus {
pub subject: serde_json::Value,
pub takedown: Option<StatusAttr>,
pub deactivated: Option<StatusAttr>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StatusAttr {
pub applied: bool,
pub r#ref: Option<String>,
}
pub async fn get_subject_status(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -43,7 +39,6 @@ pub async fn get_subject_status(
)
.into_response();
}
if params.did.is_none() && params.uri.is_none() && params.blob.is_none() {
return (
StatusCode::BAD_REQUEST,
@@ -51,7 +46,6 @@ pub async fn get_subject_status(
)
.into_response();
}
if let Some(did) = &params.did {
let user = sqlx::query!(
"SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1",
@@ -59,7 +53,6 @@ pub async fn get_subject_status(
)
.fetch_optional(&state.db)
.await;
match user {
Ok(Some(row)) => {
let deactivated = row.deactivated_at.map(|_| StatusAttr {
@@ -70,7 +63,6 @@ pub async fn get_subject_status(
applied: true,
r#ref: Some(r.clone()),
});
return (
StatusCode::OK,
Json(SubjectStatus {
@@ -101,7 +93,6 @@ pub async fn get_subject_status(
}
}
}
if let Some(uri) = &params.uri {
let record = sqlx::query!(
"SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1",
@@ -109,14 +100,12 @@ pub async fn get_subject_status(
)
.fetch_optional(&state.db)
.await;
match record {
Ok(Some(row)) => {
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
applied: true,
r#ref: Some(r.clone()),
});
return (
StatusCode::OK,
Json(SubjectStatus {
@@ -148,19 +137,16 @@ pub async fn get_subject_status(
}
}
}
if let Some(blob_cid) = &params.blob {
let blob = sqlx::query!("SELECT cid, takedown_ref FROM blobs WHERE cid = $1", blob_cid)
.fetch_optional(&state.db)
.await;
match blob {
Ok(Some(row)) => {
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
applied: true,
r#ref: Some(r.clone()),
});
return (
StatusCode::OK,
Json(SubjectStatus {
@@ -192,14 +178,12 @@ pub async fn get_subject_status(
}
}
}
(
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),
)
.into_response()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSubjectStatusInput {
@@ -207,13 +191,11 @@ pub struct UpdateSubjectStatusInput {
pub takedown: Option<StatusAttrInput>,
pub deactivated: Option<StatusAttrInput>,
}
#[derive(Deserialize)]
pub struct StatusAttrInput {
pub apply: bool,
pub r#ref: Option<String>,
}
pub async fn update_subject_status(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -227,9 +209,7 @@ pub async fn update_subject_status(
)
.into_response();
}
let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
match subject_type {
Some("com.atproto.admin.defs#repoRef") => {
let did = input.subject.get("did").and_then(|d| d.as_str());
@@ -245,7 +225,6 @@ pub async fn update_subject_status(
.into_response();
}
};
if let Some(takedown) = &input.takedown {
let takedown_ref = if takedown.apply {
takedown.r#ref.clone()
@@ -268,7 +247,6 @@ pub async fn update_subject_status(
.into_response();
}
}
if let Some(deactivated) = &input.deactivated {
let result = if deactivated.apply {
sqlx::query!(
@@ -285,7 +263,6 @@ pub async fn update_subject_status(
.execute(&mut *tx)
.await
};
if let Err(e) = result {
error!("Failed to update user deactivation status for {}: {:?}", did, e);
return (
@@ -295,7 +272,6 @@ pub async fn update_subject_status(
.into_response();
}
}
if let Err(e) = tx.commit().await {
error!("Failed to commit transaction: {:?}", e);
return (
@@ -304,14 +280,24 @@ pub async fn update_subject_status(
)
.into_response();
}
if let Some(takedown) = &input.takedown {
let status = if takedown.apply { Some("takendown") } else { None };
if let Err(e) = crate::api::repo::record::sequence_account_event(&state, did, !takedown.apply, status).await {
warn!("Failed to sequence account event for takedown: {}", e);
}
}
if let Some(deactivated) = &input.deactivated {
let status = if deactivated.apply { Some("deactivated") } else { None };
if let Err(e) = crate::api::repo::record::sequence_account_event(&state, did, !deactivated.apply, status).await {
warn!("Failed to sequence account event for deactivation: {}", e);
}
}
if let Ok(Some(handle)) = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
{
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
}
return (
StatusCode::OK,
Json(json!({
@@ -353,7 +339,6 @@ pub async fn update_subject_status(
.into_response();
}
}
return (
StatusCode::OK,
Json(json!({
@@ -392,7 +377,6 @@ pub async fn update_subject_status(
.into_response();
}
}
return (
StatusCode::OK,
Json(json!({
@@ -408,7 +392,6 @@ pub async fn update_subject_status(
}
_ => {}
}
(
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),