feat: classic ui account deletion
This commit is contained in:
@@ -236,6 +236,9 @@ pub async fn post_delete_user(
|
||||
// An admin removing their own account ends their own session too.
|
||||
Ok(()) if own => super::auth::get_logout(State(state)).await.into_response(),
|
||||
Ok(()) => axum::response::Redirect::to("/users").into_response(),
|
||||
// Same as other forbidden classic actions: a plain 403, not a redirect
|
||||
// to a page that would not show the error.
|
||||
Err(e @ DomainError::Forbidden(_)) => crate::errors::domain_error_response(e),
|
||||
Err(e) => axum::response::Redirect::to(&format!(
|
||||
"/users/{target}?error={}",
|
||||
crate::html::helpers::encode_domain_error(&e)
|
||||
|
||||
@@ -103,10 +103,11 @@
|
||||
{% if let Some(err) = error %}
|
||||
<p class="error">{{ err }}</p>
|
||||
{% endif %}
|
||||
<p style="font-size:.85em;opacity:.7">This user and all their data will be permanently deleted. Enter your own password to confirm.</p>
|
||||
<form method="POST" action="/users/{{ profile_user_id }}/delete">
|
||||
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||
<input type="password" name="password" placeholder="Your password" required autocomplete="current-password">
|
||||
<button type="submit">Delete user</button>
|
||||
<button type="submit" class="btn-small" style="color:#e57a7a;border-color:rgba(229,122,122,.3)">Delete user</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<section style="margin-top:2rem;padding-top:1rem;border-top:1px solid rgba(255,255,255,0.1)">
|
||||
<h3>Delete account</h3>
|
||||
<p style="font-size:.85em;opacity:.7;margin-bottom:.5rem">
|
||||
Permanently removes your account and everything on it: reviews, watchlist, goals, follows and uploaded images. This cannot be undone.
|
||||
Deleting your account is permanent and cannot be undone. Your reviews, watchlist, goals and follows are removed, fediverse servers are notified, and your username can never be reused.
|
||||
</p>
|
||||
<form method="post" action="/settings/account/delete">
|
||||
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||
@@ -77,7 +77,7 @@
|
||||
Confirm with your password<br>
|
||||
<input type="password" name="password" required autocomplete="current-password">
|
||||
</label>
|
||||
<button type="submit">Delete my account</button>
|
||||
<button type="submit" class="btn-small" style="color:#e57a7a;border-color:rgba(229,122,122,.3)">Delete my account</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -206,11 +206,7 @@ async fn classic_admin_delete_redirects_to_the_member_list() {
|
||||
let uri = format!("/users/{}/delete", bob.user_id);
|
||||
|
||||
let (status, headers, _) = send(&router, classic_post(&uri, &alice.cookie, PASSWORD)).await;
|
||||
assert_eq!(status, 303);
|
||||
assert!(
|
||||
location(&headers).contains("error="),
|
||||
"non-admin: {headers:?}"
|
||||
);
|
||||
assert_eq!(status, 403, "non-admin: {headers:?}");
|
||||
|
||||
promote(&pool, &alice.user_id).await;
|
||||
let (status, headers, _) = send(&router, classic_post(&uri, &alice.cookie, PASSWORD)).await;
|
||||
@@ -221,3 +217,93 @@ async fn classic_admin_delete_redirects_to_the_member_list() {
|
||||
let (status, _, _) = send(&router, get(&format!("/users/{}", bob.user_id), None)).await;
|
||||
assert_eq!(status, 404);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn classic_admin_delete_requires_the_csrf_token() {
|
||||
let (router, pool, alice, bob) = two_users().await;
|
||||
promote(&pool, &alice.user_id).await;
|
||||
let req = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(format!("/users/{}/delete", bob.user_id))
|
||||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.header(header::COOKIE, &alice.cookie)
|
||||
.body(Body::from(format!("password={PASSWORD}&_csrf=wrong")))
|
||||
.unwrap();
|
||||
let (status, _, _) = send(&router, req).await;
|
||||
assert_eq!(status, 403);
|
||||
let (status, _, _) = send(&router, get(&format!("/users/{}", bob.user_id), None)).await;
|
||||
assert_eq!(status, 200, "target must survive a CSRF rejection");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_non_admin_classic_delete_of_someone_else_is_forbidden() {
|
||||
let (router, _pool, alice, bob) = two_users().await;
|
||||
let uri = format!("/users/{}/delete", bob.user_id);
|
||||
let (status, _, _) = send(&router, classic_post(&uri, &alice.cookie, PASSWORD)).await;
|
||||
assert_eq!(status, 403);
|
||||
let (status, _, _) = send(&router, get(&format!("/users/{}", bob.user_id), None)).await;
|
||||
assert_eq!(status, 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn classic_admin_self_delete_logs_out_unless_last_admin() {
|
||||
let (router, pool, alice, bob) = two_users().await;
|
||||
promote(&pool, &alice.user_id).await;
|
||||
let uri = format!("/users/{}/delete", alice.user_id);
|
||||
|
||||
// Sole admin: refused, redirected back with an error, still logged in.
|
||||
let (status, headers, _) = send(&router, classic_post(&uri, &alice.cookie, PASSWORD)).await;
|
||||
assert_eq!(status, 303);
|
||||
assert!(location(&headers).contains("error="), "{headers:?}");
|
||||
assert!(support::cookie_value(&headers, "token").is_none());
|
||||
let (status, _, _) = send(&router, get(&format!("/users/{}", alice.user_id), None)).await;
|
||||
assert_eq!(status, 200);
|
||||
|
||||
// With a second admin the self-delete goes through and ends the session.
|
||||
promote(&pool, &bob.user_id).await;
|
||||
let (status, headers, _) = send(&router, classic_post(&uri, &alice.cookie, PASSWORD)).await;
|
||||
assert_eq!(status, 303);
|
||||
assert_eq!(location(&headers), "/");
|
||||
assert_eq!(
|
||||
support::cookie_value(&headers, "token").as_deref(),
|
||||
Some("")
|
||||
);
|
||||
let (status, _, _) = send(&router, get(&format!("/users/{}", alice.user_id), None)).await;
|
||||
assert_eq!(status, 404);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_forms_render_only_where_they_apply() {
|
||||
let (router, pool, alice, bob) = two_users().await;
|
||||
let own_delete = "action=\"/settings/account/delete\"";
|
||||
let user_delete = |id: &str| format!("action=\"/users/{id}/delete\"");
|
||||
|
||||
let (_, _, body) = send(&router, get("/settings/profile", Some(&alice.cookie))).await;
|
||||
assert!(body.contains(own_delete), "owner settings page");
|
||||
|
||||
// A non-admin viewing someone else, and anonymous viewers, see no form.
|
||||
let bob_page = format!("/users/{}", bob.user_id);
|
||||
let (_, _, body) = send(&router, get(&bob_page, Some(&alice.cookie))).await;
|
||||
assert!(!body.contains(&user_delete(&bob.user_id)), "non-admin");
|
||||
|
||||
promote(&pool, &alice.user_id).await;
|
||||
let (_, _, body) = send(&router, get(&bob_page, Some(&alice.cookie))).await;
|
||||
assert!(body.contains(&user_delete(&bob.user_id)), "admin on other");
|
||||
|
||||
let (_, _, body) = send(
|
||||
&router,
|
||||
get(&format!("/users/{}", alice.user_id), Some(&alice.cookie)),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
!body.contains(&user_delete(&alice.user_id)),
|
||||
"admin on self"
|
||||
);
|
||||
|
||||
let (_, _, body) = send(
|
||||
&router,
|
||||
get(&format!("{bob_page}?embed=true"), Some(&alice.cookie)),
|
||||
)
|
||||
.await;
|
||||
assert!(!body.contains("/delete"), "embed page");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user