~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
105 lines · 3.7 KB Raw History
//! 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,
    }))
}