mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-19 08:44:13 +00:00
Initial account deletion request endpoint
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a45ee2c7a075b27a403838b3295604e67c4213023b49e1155c4ab22e657954ff"
|
||||
}
|
||||
@@ -34,7 +34,7 @@ Lewis' corrected big boy todofile
|
||||
- [x] Implement `com.atproto.server.getAccountInviteCodes`.
|
||||
- [x] Implement `com.atproto.server.getServiceAuth` (Cross-service auth).
|
||||
- [x] Implement `com.atproto.server.listAppPasswords`.
|
||||
- [ ] Implement `com.atproto.server.requestAccountDelete`.
|
||||
- [x] Implement `com.atproto.server.requestAccountDelete`.
|
||||
- [ ] Implement `com.atproto.server.requestEmailConfirmation` / `requestEmailUpdate`.
|
||||
- [ ] Implement `com.atproto.server.requestPasswordReset` / `resetPassword`.
|
||||
- [ ] Implement `com.atproto.server.reserveSigningKey`.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS account_deletion_requests (
|
||||
token TEXT PRIMARY KEY,
|
||||
did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -7,5 +7,5 @@ pub use meta::{describe_server, health};
|
||||
pub use session::{
|
||||
activate_account, check_account_status, create_app_password, create_session,
|
||||
deactivate_account, delete_session, get_service_auth, get_session, list_app_passwords,
|
||||
refresh_session, revoke_app_password,
|
||||
refresh_session, request_account_delete, revoke_app_password,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bcrypt::verify;
|
||||
use chrono::{Duration, Utc};
|
||||
use uuid::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::{error, info, warn};
|
||||
@@ -340,6 +342,92 @@ pub async fn delete_session(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn request_account_delete(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes),
|
||||
Ok(None) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in request_account_delete: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let confirmation_token = Uuid::new_v4().to_string();
|
||||
let expires_at = Utc::now() + Duration::minutes(15);
|
||||
|
||||
let insert = sqlx::query!(
|
||||
"INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
|
||||
confirmation_token,
|
||||
did,
|
||||
expires_at
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = insert {
|
||||
error!("DB error creating deletion token: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// TODO: Send email or other notification
|
||||
info!("Account deletion requested for user {}, token: {}", did, confirmation_token);
|
||||
|
||||
(StatusCode::OK, Json(json!({}))).into_response()
|
||||
}
|
||||
|
||||
pub async fn refresh_session(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
|
||||
@@ -150,6 +150,10 @@ pub fn app(state: AppState) -> Router {
|
||||
"/xrpc/com.atproto.server.deactivateAccount",
|
||||
post(api::server::deactivate_account),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.server.requestAccountDelete",
|
||||
post(api::server::request_account_delete),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.identity.updateHandle",
|
||||
post(api::identity::update_handle),
|
||||
|
||||
@@ -201,6 +201,14 @@ async fn spawn_app(database_url: String) -> String {
|
||||
format!("http://{}", addr)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_db_connection_string() -> String {
|
||||
base_url().await;
|
||||
let container = DB_CONTAINER.get().expect("DB container not initialized");
|
||||
let port = container.get_host_port_ipv4(5432).await.expect("Failed to get port");
|
||||
format!("postgres://postgres:postgres@127.0.0.1:{}/postgres", port)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
|
||||
let res = client
|
||||
|
||||
@@ -96,6 +96,7 @@ pub async fn create_post(
|
||||
(uri, cid)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_follow(
|
||||
client: &reqwest::Client,
|
||||
follower_did: &str,
|
||||
@@ -142,6 +143,7 @@ pub async fn create_follow(
|
||||
(uri, cid)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_like(
|
||||
client: &reqwest::Client,
|
||||
liker_did: &str,
|
||||
@@ -186,6 +188,7 @@ pub async fn create_like(
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_repost(
|
||||
client: &reqwest::Client,
|
||||
reposter_did: &str,
|
||||
|
||||
@@ -442,4 +442,35 @@ async fn test_service_auth_lifecycle() {
|
||||
assert_eq!(claims["iss"], did);
|
||||
assert_eq!(claims["aud"], "did:web:api.bsky.app");
|
||||
assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_account_delete() {
|
||||
let client = client();
|
||||
let (did, jwt) = setup_new_user("request-delete-test").await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestAccountDelete",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request account deletion");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let db_url = get_db_connection_string().await;
|
||||
let pool = sqlx::PgPool::connect(&db_url).await.expect("Failed to connect to test DB");
|
||||
|
||||
let row = sqlx::query!("SELECT token, expires_at FROM account_deletion_requests WHERE did = $1", did)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.expect("Failed to query DB");
|
||||
|
||||
assert!(row.is_some(), "Deletion token should exist in DB");
|
||||
let row = row.unwrap();
|
||||
assert!(!row.token.is_empty(), "Token should not be empty");
|
||||
assert!(row.expires_at > Utc::now(), "Token should not be expired");
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use helpers::*;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header;
|
||||
use serde_json::{Value, json};
|
||||
use chrono::Utc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_latest_commit_success() {
|
||||
|
||||
Reference in New Issue
Block a user