# retoor import logging from typing import Annotated from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from devplacepy.dependencies import json_or_form from devplacepy.models import AccountDeleteForm from devplacepy.responses import action_result, json_error, respond from devplacepy.routers.profile._shared import resolve_customization_target from devplacepy.schemas import AccountDeletionOut from devplacepy.seo import base_seo_context from devplacepy.services.audit import record as audit from devplacepy.services.moderation import deletion from devplacepy.utils import get_current_user, verify_password_async logger = logging.getLogger(__name__) router = APIRouter() REMOVED = [ "Your account record, username, email address and password", "Your profile: bio, location, links and avatar", "Your posts, comments, gists, projects, project files and quizzes", "Your uploads and media gallery", "Your direct-message history, votes, reactions, bookmarks and polls", "Your API key, access tokens and every signed-in session", "Your assistant conversations, tasks, lessons and custom tools", ] RETAINED = [ "Append-only audit and moderation records, which hold identifiers rather than " "your profile, so the platform can show it enforced its own rules", "Backup archives, until they rotate out on their normal schedule", ] def _owner_only( request: Request, username: str, message: str = "Only the account holder can delete this account", ): target, denied = resolve_customization_target(request, username) if denied is not None: return None, denied viewer = get_current_user(request) if not viewer or viewer["uid"] != target["uid"]: audit.record( request, "security.authz.denied", user=viewer, result="denied", target_type="user", target_uid=target["uid"], target_label=target["username"], metadata={"reason": message}, summary=f"non-owner denied {request.method} {request.url.path}", links=[audit.target("user", target["uid"], target["username"])], ) return None, json_error(403, message) return target, None async def _password_matches(password: str, hashed: str) -> bool: if not hashed: return False try: return await verify_password_async(password, hashed) except ValueError: return False @router.get("/{username}/delete", response_class=HTMLResponse) async def delete_account_page(request: Request, username: str): target, denied = _owner_only(request, username) if denied is not None: return denied seo_ctx = base_seo_context( request, title="Delete your account", description="Permanently remove your DevPlace account and personal data.", robots="noindex,nofollow", breadcrumbs=[ {"name": "Home", "url": "/feed"}, {"name": target["username"], "url": f"/profile/{target['username']}"}, { "name": "Delete account", "url": f"/profile/{target['username']}/delete", }, ], ) return respond( request, "account_delete.html", { **seo_ctx, "request": request, "user": target, "username": target["username"], "grace_hours": deletion.grace_hours(), "removed": REMOVED, "retained": RETAINED, }, model=AccountDeletionOut, ) @router.post("/{username}/delete") async def delete_account( request: Request, username: str, data: Annotated[AccountDeleteForm, Depends(json_or_form(AccountDeleteForm))], ): target, denied = _owner_only(request, username) if denied is not None: return denied if not await _password_matches(data.password, target.get("password_hash", "")): audit.record( request, "account.delete.request", result="denied", target_type="user", target_uid=target["uid"], target_label=target["username"], summary=f"account deletion for {target['username']} refused: wrong password", links=[audit.target("user", target["uid"], target["username"])], ) return json_error(403, "That password is not correct") result = deletion.delete_account(target) if result is None: return json_error(409, "This account is already being deleted") logger.info(f"Account {target['username']} deleted by request") audit.record( request, "account.delete.request", target_type="user", target_uid=target["uid"], target_label=target["username"], metadata={ "stamp": result["stamp"], "rows": result["rows"], "grace_hours": result["grace_hours"], }, summary=f"account {target['username']} deleted", links=[audit.target("user", target["uid"], target["username"])], ) response = action_result(request, "/", data=result) response.delete_cookie("session") return response