@router.post("/accounts/{account_id}/revoke") async def disconnect_account(account_id: int, user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): """Disconnect: revoke token at Google and delete stored credentials.""" account = await db.get(GmailAccount, account_id) if not account or account.user_id != user.id: raise HTTPException(404, "Account not found") token = account.oauth_token revoked_at_google = False if token: from app.services.gmail_service import revoke_token revoked_at_google = revoke_token(token.refresh_token or token.access_token) account.status = "revoked" account.is_active = False account.revoked_by_user = True account.revoked_at = datetime.now(timezone.utc) if token: await db.delete(token) await db.commit() await log_audit(db, user.id, "gmail.disconnect", "gmail_account", account.id, {"email": account.email, "revoked_at_google": revoked_at_google}, request.client.host if request else None) return {"ok": True, "revoked_at_google": revoked_at_google} @router.delete("/accounts/{account_id}") async def delete_account(account_id: int, user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): """DELETE semantics: revoke token at Google (best-effort) and REMOVE the account row (oauth_tokens cascade; campaign_recipients/send_* FKs are SET NULL).""" account = await db.get(GmailAccount, account_id) if not account or account.user_id != user.id: raise HTTPException(404, "Account not found") revoked_at_google = False token = account.oauth_token if token: revoked_at_google = gmail_service.revoke_token(token.refresh_token or token.access_token) email = account.email await db.delete(account) # token row cascades (delete-orphan + FK CASCADE) await db.commit() await log_audit(db, user.id, "gmail.delete", "gmail_account", account_id, {"email": email, "revoked_at_google": revoked_at_google}, request.client.host if request else None) return {"ok": True, "removed": True, "revoked_at_google": revoked_at_google} @router.post("/accounts/clear-all") async def clear_all_accounts(user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): """Clear All: revoke tokens at Google (best-effort) and remove all locally saved Gmail account rows. Audit trail preserved.""" rows = (await db.execute(select(GmailAccount).where( GmailAccount.user_id == user.id))).scalars().all() revoked = 0 for account in rows: token = account.oauth_token if token: if gmail_service.revoke_token(token.refresh_token or token.access_token): revoked += 1 await db.delete(account) # token cascades; related FKs are SET NULL await db.commit() await log_audit(db, user.id, "gmail.clear_all", "gmail_account", None, {"count": len(rows), "revoked_at_google": revoked}, request.client.host if request else None) return {"ok": True, "removed": len(rows), "revoked_at_google": revoked}