Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion app/admin_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,7 @@ def delete_partner_link(self):
return redirect(url_for("admin.email_search.index", query=user_id))

AdminAuditLog.create(
admin_user_id=user.id,
admin_user_id=current_user.id,
model=User.__class__.__name__,
model_id=user.id,
action=AuditLogActionEnum.unlink_user.value,
Expand All @@ -979,6 +979,67 @@ def delete_partner_link(self):

return redirect(url_for("admin.email_search.index", query=user_id))

@expose("/stop_soft_delete_user", methods=["POST"])
def stop_soft_delete_user(self):
user_id = request.form.get("user_id")
if not user_id:
flash("Missing user_id", "error")
return redirect(url_for("admin.email_search.index"))
try:
user_id = int(user_id)
except ValueError:
flash("Missing user_id", "error")
return redirect(url_for("admin.email_search.index", query=user_id))
user = User.get(user_id)
if user is None:
flash("User not found", "error")
return redirect(url_for("admin.email_search.index", query=user_id))
if user.delete_on is None:
flash("User is not pending deletion", "error")
return redirect(url_for("admin.email_search.index", query=user_id))

user.delete_on = None
AdminAuditLog.create(
admin_user_id=current_user.id,
model=User.__class__.__name__,
model_id=user.id,
action=AuditLogActionEnum.stop_soft_delete_user.value,
data={"user_id": user_id},
)
Session.commit()

return redirect(url_for("admin.email_search.index", query=user_id))

@expose("/force_soft_delete_user", methods=["POST"])
def force_soft_delete_user(self):
user_id = request.form.get("user_id")
if not user_id:
flash("Missing user_id", "error")
return redirect(url_for("admin.email_search.index"))
try:
user_id = int(user_id)
except ValueError:
flash("Missing user_id", "error")
return redirect(url_for("admin.email_search.index", query=user_id))
user = User.get(user_id)
if user is None:
flash("User not found", "error")
return redirect(url_for("admin.email_search.index", query=user_id))
if user.delete_on is None:
flash("User is not pending deletion", "error")
return redirect(url_for("admin.email_search.index", query=user_id))
User.delete(user.id)
AdminAuditLog.create(
admin_user_id=current_user.id,
model=User.__class__.__name__,
model_id=user.id,
action=AuditLogActionEnum.delete_object.value,
data={"user_id": user_id},
)
Session.commit()

return redirect(url_for("admin.email_search.index", query=user_id))


class CustomDomainWithValidationData:
def __init__(self, domain: CustomDomain):
Expand Down
22 changes: 3 additions & 19 deletions app/api/views/user.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,18 @@
from flask import jsonify, g
from sqlalchemy_utils.types.arrow import arrow

from app.api.base import api_bp, require_api_sudo, require_api_auth
from app.constants import JobType
from app.extensions import limiter
from app.log import LOG
from app.models import Job, ApiToCookieToken
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.models import ApiToCookieToken
from app.user_utils import soft_delete_user


@api_bp.route("/user", methods=["DELETE"])
@require_api_sudo
def delete_user():
"""
Delete the user. Requires sudo mode.

"""
# Schedule delete account job
emit_user_audit_log(
user=g.user,
action=UserAuditLogAction.UserMarkedForDeletion,
message=f"Marked user {g.user.id} ({g.user.email}) for deletion from API",
)
LOG.w("schedule delete account job for %s", g.user)
Job.create(
name=JobType.DELETE_ACCOUNT.value,
payload={"user_id": g.user.id},
run_at=arrow.now(),
commit=True,
)
soft_delete_user(g.user, "API")
return jsonify(ok=True)


Expand Down
2 changes: 1 addition & 1 deletion app/auth/views/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def login():
LoginEvent(LoginEvent.ActionType.disabled_login).send()
elif user.delete_on is not None:
flash(
f"Your account is scheduled to be deleted on {user.delete_on}",
"Email or password incorrect",
"error",
)
LoginEvent(LoginEvent.ActionType.scheduled_to_be_deleted).send()
Expand Down
28 changes: 4 additions & 24 deletions app/dashboard/views/delete_account.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import arrow
from flask import flash, redirect, url_for, request, render_template
from flask_login import login_required, current_user
from flask_wtf import FlaskForm

from app.constants import JobType
from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.log import LOG
from app.models import Subscription, Job
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.models import Subscription
from app.user_utils import soft_delete_user


class DeleteDirForm(FlaskForm):
Expand All @@ -32,25 +29,8 @@ def delete_account():
flash("Please cancel your current subscription first", "warning")
return redirect(url_for("dashboard.setting"))

# Schedule delete account job
LOG.w("schedule delete account job for %s", current_user)
emit_user_audit_log(
user=current_user,
action=UserAuditLogAction.UserMarkedForDeletion,
message=f"User {current_user.id} ({current_user.email}) marked for deletion via webapp",
)
Job.create(
name=JobType.DELETE_ACCOUNT.value,
payload={"user_id": current_user.id},
run_at=arrow.now(),
commit=True,
)

flash(
"Your account deletion has been scheduled. "
"You'll receive an email when the deletion is finished",
"info",
)
soft_delete_user(current_user, "webapp")
flash("Your account deletion has been scheduled", "info")
return redirect(url_for("dashboard.setting"))

return render_template("dashboard/delete_account.html", delete_form=delete_form)
1 change: 1 addition & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ class AuditLogActionEnum(EnumE):
stop_trial = 11
unlink_user = 12
delete_custom_domain = 13
stop_soft_delete_user = 14


class Phase(EnumE):
Expand Down
20 changes: 20 additions & 0 deletions app/user_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import arrow

from app.db import Session
from app.log import LOG
from app.models import User, ApiKey
from app.session import logout_session
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction


def soft_delete_user(user: User, source: str):
LOG.i(f"Marked user {user} for soft-deletion from {source}")
emit_user_audit_log(
user=user,
action=UserAuditLogAction.UserMarkedForDeletion,
message=f"Marked user {user} ({user.email}) for deletion from {source}",
)
user.delete_on = arrow.utcnow()
ApiKey.filter_by(user_id=user.id).delete()
Session.commit()
logout_session()
2 changes: 1 addition & 1 deletion email_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
if reply_contact:
reply_to_contact.append(reply_contact)

if alias.user.delete_on is not None:
if not alias.user.is_active():
LOG.d(f"user {user} is pending to be deleted. Do not forward")
EmailLog.create(
contact_id=contact.id,
Expand Down
Loading
Loading