fixed account deletion

This commit is contained in:
nvrl committed 2026-06-16 20:05:40 +02:00
1 parent 2f2935a1cb
commit 296ae167fb
3 files changed
+45 -7

No files matched your search

+1 -4
View File
@@ -492,10 +492,7 @@ async fn discover(
cond = cond.add(rating::Column::FlavorId.is_in(flavor_ids));
}
let count = Rating::find()
.filter(cond.clone())
.count(&state.db)
.await?;
let count = Rating::find().filter(cond.clone()).count(&state.db).await?;
let mut query = Rating::find().filter(cond);
query = if top {
query
+29 -2
View File
@@ -353,7 +353,12 @@ async fn suggested(
// users_by_ids orders by id; restore the activity ranking afterwards.
let users = users_by_ids(&state, &ranked).await?;
let mut out = build_users(&state, &ctx, users, Some(uid)).await?;
out.sort_by_key(|u| ranked.iter().position(|id| *id == u.id).unwrap_or(usize::MAX));
out.sort_by_key(|u| {
ranked
.iter()
.position(|id| *id == u.id)
.unwrap_or(usize::MAX)
});
Ok(Json(out))
}
@@ -1045,12 +1050,21 @@ async fn request_account_deletion(
active.deletion_code_expires = Set(Some(expires));
active.update(&state.db).await?;
let link = format!(
"{}/settings?deletion_code={}",
state.config.frontend_url, code
);
crate::email::spawn_mail(
state.config.email.clone(),
"Confirm Account Deletion - Holy Flavors Archive".into(),
format!(
"Hi {uname},\n\nYou requested to delete your account. \
This action is permanent.\n\nYour deletion code is: {code}"
This action is permanent and cannot be undone.\n\n\
Your deletion code is: {code}\n\n\
Or open this link (while signed in) to pre-fill the code, then \
confirm the deletion on the page:\n{link}\n\n\
If you didn't request this, you can safely ignore this email — \
nothing will be deleted without confirmation."
),
vec![email],
);
@@ -1138,6 +1152,19 @@ pub(crate) async fn delete_user_cascade(state: &AppState, uid: i32) -> ApiResult
"DELETE FROM api_user_following WHERE from_user_id = ?1 OR to_user_id = ?1",
))
.await?;
// Legacy Django tables that still carry FKs to api_user: the DRF auth token,
// the admin action log, and the (empty but constrained) group / permission
// M2M join tables. SQLite blocks the user delete unless these go first.
txn.execute(exec("DELETE FROM authtoken_token WHERE user_id = ?"))
.await?;
txn.execute(exec("DELETE FROM django_admin_log WHERE user_id = ?"))
.await?;
txn.execute(exec("DELETE FROM api_user_groups WHERE user_id = ?"))
.await?;
txn.execute(exec(
"DELETE FROM api_user_user_permissions WHERE user_id = ?",
))
.await?;
// Token blacklist tables reference the user via outstanding tokens.
txn.execute(exec(
"DELETE FROM token_blacklist_blacklistedtoken WHERE token_id IN \
+15 -1
View File
@@ -28,7 +28,7 @@ import {
Switch,
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../hooks/useAuth';
@@ -49,6 +49,7 @@ const readErr = (err: unknown): string | undefined =>
const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const queryClient = useQueryClient();
const { refetchUser } = useAuth();
useTitle(t('nav.settings'));
@@ -78,6 +79,19 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [selectedBannerId, setSelectedBannerId] = useState<number | string>('');
// Deep link from the deletion email: ?deletion_code=NNNNNN pre-fills the code
// and opens the confirm form. The user still has to click "Delete" — the link
// is a convenience, not a one-click destruct. Strip the param after reading so
// it doesn't linger in the URL / browser history.
useEffect(() => {
const code = searchParams.get('deletion_code');
if (!code) return;
setDeletionCode(code);
setIsDeleting(true);
searchParams.delete('deletion_code');
setSearchParams(searchParams, { replace: true });
}, [searchParams, setSearchParams]);
const handleGoBack = () => {
if (window.history.length > 1) navigate(-1);
else navigate('/');