Added soft deletes to all deletion functions and added deleted_at filters as required

This commit is contained in:
2026-04-22 17:27:49 +09:30
parent 33a7fdac59
commit eb2fc6c8b3
18 changed files with 64 additions and 57 deletions
+5 -6
View File
@@ -36,9 +36,9 @@ class AuthService:
Raises:
EmailAlreadyExistsError: If email is already registered
"""
# Check if email already exists
existing_user = User.query.filter_by(email=email.lower()).first()
if existing_user and existing_user.deleted_at is None:
# Check if email already exists
existing_user = User.query.filter_by(email=email.lower(), deleted_at=None).first()
if existing_user:
raise EmailAlreadyExistsError()
# Create user
@@ -280,12 +280,11 @@ class AuthService:
raise ConflictError("TOTP is already enabled for this account")
# Clean up any existing unverified TOTP enrollment attempts
# Use hard delete for unverified methods since they're incomplete enrollment attempts
# Soft delete for unverified methods since they're incomplete enrollment attempts
existing_totp_method = user.get_totp_method()
if existing_totp_method and not existing_totp_method.verified:
logger.debug(f"Removing existing unverified TOTP method for user {user.id}")
db.session.delete(existing_totp_method) # Hard delete - unverified methods are temporary
db.session.commit() # Commit to ensure deletion before creating new record
existing_totp_method.delete(soft=True) # Soft delete - unverified methods are temporary
# Generate TOTP secret
secret = TOTPService.generate_secret()
+6 -6
View File
@@ -50,8 +50,8 @@ class BillingService:
if not plan:
raise ValueError("Plan not found")
# Check if subscription already exists
existing = Subscription.query.filter_by(organization_id=organization_id).first()
# Check if subscription already exists
existing = Subscription.query.filter_by(organization_id=organization_id, deleted_at=None).first()
if existing:
raise ValueError("Organization already has a subscription")
@@ -88,7 +88,7 @@ class BillingService:
Returns:
Updated subscription
"""
subscription = Subscription.query.filter_by(organization_id=organization_id).first()
subscription = Subscription.query.filter_by(organization_id=organization_id, deleted_at=None).first()
if not subscription:
raise ValueError("No subscription found for organization")
@@ -111,7 +111,7 @@ class BillingService:
Returns:
Updated subscription
"""
subscription = Subscription.query.filter_by(organization_id=organization_id).first()
subscription = Subscription.query.filter_by(organization_id=organization_id, deleted_at=None).first()
if not subscription:
raise ValueError("No subscription found for organization")
@@ -132,7 +132,7 @@ class BillingService:
Returns:
Updated subscription
"""
subscription = Subscription.query.filter_by(organization_id=organization_id).first()
subscription = Subscription.query.filter_by(organization_id=organization_id, deleted_at=None).first()
if not subscription:
raise ValueError("No subscription found for organization")
@@ -158,7 +158,7 @@ class BillingService:
Returns:
Overage calculation with details
"""
subscription = Subscription.query.filter_by(organization_id=organization_id).first()
subscription = Subscription.query.filter_by(organization_id=organization_id, deleted_at=None).first()
if not subscription:
return {"has_overage": False, "overage_cost": 0, "user_count": 0, "included_users": 0}
@@ -42,7 +42,7 @@ class ExternalAuthService:
provider_type_str = provider_type.value if isinstance(provider_type, AuthMethodType) else provider_type
app_config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type_str
provider_type=provider_type_str, deleted_at=None
).first()
if not app_config:
@@ -64,6 +64,7 @@ class ExternalAuthService:
org_override_obj = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id,
provider_type=provider_type_str,
deleted_at=None,
).first()
if org_override_obj and not org_override_obj.is_enabled:
@@ -14,7 +14,7 @@ def create_app_provider_config(
**kwargs,
) -> ApplicationProviderConfig:
existing = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type
provider_type=provider_type, deleted_at=None
).first()
if existing:
@@ -51,7 +51,7 @@ def update_app_provider_config(
**updates,
) -> ApplicationProviderConfig:
config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type
provider_type=provider_type, deleted_at=None
).first()
if not config:
@@ -90,7 +90,7 @@ def update_app_provider_config(
def get_app_provider_config(provider_type: str) -> ApplicationProviderConfig:
config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type
provider_type=provider_type, deleted_at=None
).first()
if not config:
@@ -104,13 +104,13 @@ def get_app_provider_config(provider_type: str) -> ApplicationProviderConfig:
def list_app_provider_configs() -> list:
configs = ApplicationProviderConfig.query.all()
configs = ApplicationProviderConfig.query.filter_by(deleted_at=None).all()
return [config.to_dict() for config in configs]
def delete_app_provider_config(provider_type: str) -> bool:
config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type
provider_type=provider_type, deleted_at=None
).first()
if not config:
@@ -219,10 +219,11 @@ def authenticate_with_provider(
auth_method = AuthenticationMethod.query.filter_by(
method_type=provider_type,
provider_user_id=user_info["provider_user_id"],
deleted_at=None,
).first()
if not auth_method:
existing_user = User.query.filter_by(email=user_info["email"]).first()
existing_user = User.query.filter_by(email=user_info["email"], deleted_at=None).first()
if existing_user:
AuditService.log_external_auth_login_failed(
@@ -286,12 +287,13 @@ def unlink_provider(
auth_method = AuthenticationMethod.query.filter_by(
user_id=user_id,
method_type=provider_type,
deleted_at=None,
).first()
if not auth_method:
raise ExternalAuthError("Provider not linked", "PROVIDER_NOT_LINKED", 400)
other_methods = AuthenticationMethod.query.filter_by(user_id=user_id).count()
other_methods = AuthenticationMethod.query.filter_by(user_id=user_id, deleted_at=None).count()
if other_methods <= 1:
raise ExternalAuthError(
"Cannot unlink the last authentication method",
@@ -16,7 +16,7 @@ def create_org_provider_override(
**kwargs,
) -> OrganizationProviderOverride:
app_config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type
provider_type=provider_type, deleted_at=None
).first()
if not app_config:
@@ -29,6 +29,7 @@ def create_org_provider_override(
existing = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id,
provider_type=provider_type,
deleted_at=None,
).first()
if existing:
@@ -69,6 +70,7 @@ def update_org_provider_override(
override = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id,
provider_type=provider_type,
deleted_at=None,
).first()
if not override:
@@ -110,6 +112,7 @@ def get_org_provider_override(
override = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id,
provider_type=provider_type,
deleted_at=None,
).first()
if not override:
@@ -124,7 +127,7 @@ def get_org_provider_override(
def list_org_provider_overrides(organization_id: str) -> list:
overrides = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id
organization_id=organization_id, deleted_at=None
).all()
return [override.to_dict() for override in overrides]
@@ -133,6 +136,7 @@ def delete_org_provider_override(organization_id: str, provider_type: str) -> bo
override = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id,
provider_type=provider_type,
deleted_at=None,
).first()
if not override:
@@ -1024,17 +1024,17 @@ def revoke_membership_soft(
def hard_delete_membership(membership_id: str) -> None:
"""Hard delete a membership after ZeroTier has been cleaned up.
"""Soft delete a membership after ZeroTier has been cleaned up.
Called by the reconciliation job after successfully removing the member
from the ZeroTier controller. This is the final DB cleanup step.
from the ZeroTier controller. This marks the membership as deleted.
"""
membership = DeviceNetworkMembership.query.filter(
DeviceNetworkMembership.id == membership_id,
).first()
if not membership:
logger.warning(f"[hard_delete_membership] Membership {membership_id} not found, skipping.")
logger.warning(f"[hard_delete_membership] Membership {membership_id} not found or already deleted, skipping.")
return
device = Device.query.get(membership.device_id)
@@ -1048,7 +1048,7 @@ def hard_delete_membership(membership_id: str) -> None:
except Exception as exc:
logger.warning(f"[hard_delete_membership] ZT delete failed for {device.node_id}: {exc}")
db.session.delete(membership)
membership.delete(soft=True)
db.session.commit()
AuditService.log_action(
@@ -1061,6 +1061,6 @@ def hard_delete_membership(membership_id: str) -> None:
"device_node_id": device.node_id if device else None,
"network_id": network.zerotier_network_id if network else None,
},
description=f"Membership hard-deleted: device {device.node_id if device else 'unknown'} from network",
description=f"Membership deleted: device {device.node_id if device else 'unknown'} from network",
success=True,
)
@@ -111,7 +111,7 @@ def handle_register_callback(
access_token=tokens["access_token"],
)
existing_user = User.query.filter_by(email=user_info["email"]).first()
existing_user = User.query.filter_by(email=user_info["email"], deleted_at=None).first()
if existing_user:
raise OAuthFlowError(
f"An account with email {user_info['email']} already exists. "
@@ -283,9 +283,9 @@ def reconcile_deleted_memberships() -> dict:
if not device or not network:
logger.warning(
f"[Reconciliation] Membership {membership.id}: missing "
f"{'device' if not device else 'network'}hard-deleting record only."
f"{'device' if not device else 'network'}soft-deleting record only."
)
db.session.delete(membership)
membership.delete(soft=True)
db.session.commit()
results["deleted"] += 1
continue
@@ -304,20 +304,20 @@ def reconcile_deleted_memberships() -> dict:
except Exception as zt_exc:
logger.warning(
f"[Reconciliation] ZT delete failed for node {node_id} "
f"on {network_label}: {zt_exc} — proceeding with DB hard-delete."
f"on {network_label}: {zt_exc} — proceeding with DB soft-delete."
)
db.session.delete(membership)
membership.delete(soft=True)
db.session.commit()
results["deleted"] += 1
logger.debug(
f"[Reconciliation] Hard-deleted membership {membership.id} "
f"[Reconciliation] Soft-deleted membership {membership.id} "
f"(node={node_id}, network={network_label})."
)
except Exception as exc:
logger.error(
f"[Reconciliation] Failed to hard-delete membership {membership.id}: {exc}",
f"[Reconciliation] Failed to soft-delete membership {membership.id}: {exc}",
exc_info=True,
)
results["errors"] += 1