johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! An account deleting its knotless data (`getAccountData`,
//! `deleteAccountData`): every record it holds in a `dev.knotless.*`
//! collection, and the spaces of its private repositories, through the
//! same code as `knotless account delete-data` ([`knotless::account`]); then what
//! this server holds from it, as for an account deleted at its PDS
//! ([`crate::accounts::forget`]).
//!
//! The pages ask with their cookie, and the records are deleted with the
//! session the server keeps for the account, a part a call, so the page
//! can say how far it has got and a run cut short carries on. The command
//! line and MCP delete the records themselves and ask with a service
//! token only to be forgotten, which is done once the account's PDS holds
//! no knotless record.
use super::stars::logged_in;
use super::*;
use crate::viewer::Viewer;
use axum::Extension;
use knotless::account::{self, Step};
/// The routes.
pub fn routes() -> Router<Arc<App>> {
Router::new()
.route("/xrpc/dev.knotless.getAccountData", get(get_account_data))
.route(
&format!("/xrpc/{}", account::FORGET),
post(delete_account_data),
)
}
/// `dev.knotless.getAccountData`.
pub async fn get_account_data(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
) -> Result<Response> {
let did = logged_in(&app, viewer, &headers, "see its knotless data")?;
let ctx = crate::login::ctx_for(&app, &did);
let held = account::inventory(&ctx).await.map_err(AppError::upstream)?;
Ok(answer(held))
}
#[derive(Deserialize)]
pub struct DeleteInput {
did: String,
limit: Option<u64>,
}
/// `dev.knotless.deleteAccountData`.
pub async fn delete_account_data(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<DeleteInput>,
) -> Result<Response> {
if headers.contains_key(header::AUTHORIZATION) {
return forgotten(&app, viewer, &input).await;
}
let did = logged_in(&app, viewer, &headers, "delete its knotless data")?;
named(&did, &input)?;
let ctx = crate::login::ctx_for(&app, &did);
let limit = input.limit.unwrap_or(400).clamp(1, 1000);
let step = account::delete_some(&ctx, limit).await;
let step = step.map_err(AppError::upstream)?;
if !step.done {
return Ok(answer(step));
}
crate::accounts::forget(&app, &did).await;
let gw = crate::gateway::gateway(&app)?;
let mut res = answer(step);
crate::login::moved_on(&app, gw, &headers, &mut res);
Ok(res)
}
/// Refuses a call naming another account than the one asking.
fn named(did: &str, input: &DeleteInput) -> Result<()> {
if input.did == did {
return Ok(());
}
let why = format!("{did} asks, but names {}", input.did);
Err(Shown(StatusCode::FORBIDDEN, why))
}
/// Forgets the account a service token names, once its PDS holds no
/// knotless record: it deleted them itself.
async fn forgotten(app: &App, viewer: Viewer, input: &DeleteInput) -> Result<Response> {
let why = "ask with a service token, or logged in to the pages";
let did = viewer
.0
.ok_or(Shown(StatusCode::UNAUTHORIZED, why.into()))?;
named(&did, input)?;
let holds = account::holds_any(&app.ctx, &did).await;
if holds.map_err(AppError::upstream)? {
let failure = Failure {
error: "RecordsRemain".into(),
message: format!("{did} still holds knotless records: delete them first"),
};
return Ok((StatusCode::BAD_REQUEST, axum::Json(failure)).into_response());
}
crate::accounts::forget(app, &did).await;
Ok(answer(Step {
deleted: 0,
done: true,
}))
}