mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed account deletion
This commit is contained in:
1 parent
2f2935a1cb
commit
296ae167fb
3 files changed
+45
-7
No files matched your search
@@ -492,10 +492,7 @@ async fn discover(
|
|||||||
cond = cond.add(rating::Column::FlavorId.is_in(flavor_ids));
|
cond = cond.add(rating::Column::FlavorId.is_in(flavor_ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = Rating::find()
|
let count = Rating::find().filter(cond.clone()).count(&state.db).await?;
|
||||||
.filter(cond.clone())
|
|
||||||
.count(&state.db)
|
|
||||||
.await?;
|
|
||||||
let mut query = Rating::find().filter(cond);
|
let mut query = Rating::find().filter(cond);
|
||||||
query = if top {
|
query = if top {
|
||||||
query
|
query
|
||||||
|
|||||||
@@ -353,7 +353,12 @@ async fn suggested(
|
|||||||
// users_by_ids orders by id; restore the activity ranking afterwards.
|
// users_by_ids orders by id; restore the activity ranking afterwards.
|
||||||
let users = users_by_ids(&state, &ranked).await?;
|
let users = users_by_ids(&state, &ranked).await?;
|
||||||
let mut out = build_users(&state, &ctx, users, Some(uid)).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))
|
Ok(Json(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,12 +1050,21 @@ async fn request_account_deletion(
|
|||||||
active.deletion_code_expires = Set(Some(expires));
|
active.deletion_code_expires = Set(Some(expires));
|
||||||
active.update(&state.db).await?;
|
active.update(&state.db).await?;
|
||||||
|
|
||||||
|
let link = format!(
|
||||||
|
"{}/settings?deletion_code={}",
|
||||||
|
state.config.frontend_url, code
|
||||||
|
);
|
||||||
crate::email::spawn_mail(
|
crate::email::spawn_mail(
|
||||||
state.config.email.clone(),
|
state.config.email.clone(),
|
||||||
"Confirm Account Deletion - Holy Flavors Archive".into(),
|
"Confirm Account Deletion - Holy Flavors Archive".into(),
|
||||||
format!(
|
format!(
|
||||||
"Hi {uname},\n\nYou requested to delete your account. \
|
"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],
|
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",
|
"DELETE FROM api_user_following WHERE from_user_id = ?1 OR to_user_id = ?1",
|
||||||
))
|
))
|
||||||
.await?;
|
.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.
|
// Token blacklist tables reference the user via outstanding tokens.
|
||||||
txn.execute(exec(
|
txn.execute(exec(
|
||||||
"DELETE FROM token_blacklist_blacklistedtoken WHERE token_id IN \
|
"DELETE FROM token_blacklist_blacklistedtoken WHERE token_id IN \
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
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 { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
@@ -49,6 +49,7 @@ const readErr = (err: unknown): string | undefined =>
|
|||||||
const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { refetchUser } = useAuth();
|
const { refetchUser } = useAuth();
|
||||||
useTitle(t('nav.settings'));
|
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 [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||||
const [selectedBannerId, setSelectedBannerId] = useState<number | string>('');
|
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 = () => {
|
const handleGoBack = () => {
|
||||||
if (window.history.length > 1) navigate(-1);
|
if (window.history.length > 1) navigate(-1);
|
||||||
else navigate('/');
|
else navigate('/');
|
||||||
|
|||||||
Reference in new issue
Block a user