Added soft deletes to all deletion functions and added deleted_at filters as required
This commit is contained in:
@@ -21,7 +21,7 @@ def admin_list_app_providers():
|
||||
return api_response(success=False, message="Admin access required", status=403, error_type="FORBIDDEN")
|
||||
|
||||
PROVIDERS = [{"id": "google", "name": "Google"}, {"id": "github", "name": "GitHub"}, {"id": "microsoft", "name": "Microsoft"}]
|
||||
db_configs = {c.provider_type: c for c in ApplicationProviderConfig.query.all()}
|
||||
db_configs = {c.provider_type: c for c in ApplicationProviderConfig.query.filter_by(deleted_at=None).all()}
|
||||
|
||||
result = []
|
||||
for p in PROVIDERS:
|
||||
@@ -64,7 +64,7 @@ def admin_configure_app_provider(provider: str):
|
||||
if not client_id:
|
||||
return api_response(success=False, message="client_id is required", status=400, error_type="VALIDATION_ERROR")
|
||||
|
||||
cfg = ApplicationProviderConfig.query.filter_by(provider_type=provider).first()
|
||||
cfg = ApplicationProviderConfig.query.filter_by(provider_type=provider, deleted_at=None).first()
|
||||
if cfg:
|
||||
cfg.client_id = client_id
|
||||
if client_secret:
|
||||
@@ -90,7 +90,6 @@ def admin_delete_app_provider(provider: str):
|
||||
from gatehouse_app.models.auth.authentication_method import ApplicationProviderConfig
|
||||
from gatehouse_app.models import OrganizationMember
|
||||
from gatehouse_app.utils.constants import OrganizationRole
|
||||
from gatehouse_app.extensions import db
|
||||
|
||||
admin_memberships = OrganizationMember.query.filter(
|
||||
OrganizationMember.user_id == g.current_user.id,
|
||||
@@ -100,10 +99,9 @@ def admin_delete_app_provider(provider: str):
|
||||
if not admin_memberships:
|
||||
return api_response(success=False, message="Admin access required", status=403, error_type="FORBIDDEN")
|
||||
|
||||
cfg = ApplicationProviderConfig.query.filter_by(provider_type=provider).first()
|
||||
cfg = ApplicationProviderConfig.query.filter_by(provider_type=provider, deleted_at=None).first()
|
||||
if not cfg:
|
||||
return api_response(success=False, message=f"Provider '{provider}' is not configured", status=404, error_type="NOT_FOUND")
|
||||
|
||||
db.session.delete(cfg)
|
||||
db.session.commit()
|
||||
cfg.delete()
|
||||
return api_response(message=f"{provider.capitalize()} OAuth provider configuration removed")
|
||||
|
||||
@@ -174,6 +174,7 @@ def select_organization():
|
||||
|
||||
auth_method = AuthenticationMethod.query.filter_by(
|
||||
method_type=state_record.provider_type,
|
||||
deleted_at=None,
|
||||
).order_by(AuthenticationMethod.created_at.desc()).first()
|
||||
|
||||
if not auth_method:
|
||||
@@ -181,11 +182,11 @@ def select_organization():
|
||||
|
||||
user = auth_method.user
|
||||
|
||||
org = Organization.query.get(organization_id)
|
||||
org = Organization.query.filter_by(id=organization_id, deleted_at=None).first()
|
||||
if not org:
|
||||
return api_response(success=False, message="Organization not found", status=404, error_type="NOT_FOUND")
|
||||
|
||||
member = OrganizationMember.query.filter_by(user_id=user.id, organization_id=organization_id).first()
|
||||
member = OrganizationMember.query.filter_by(user_id=user.id, organization_id=organization_id, deleted_at=None).first()
|
||||
if not member:
|
||||
return api_response(success=False, message="You are not a member of this organization", status=403, error_type="FORBIDDEN")
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ from gatehouse_app.api.v1.external_auth._helpers import get_provider_type, _get_
|
||||
def list_providers():
|
||||
from gatehouse_app.models.auth.authentication_method import ApplicationProviderConfig
|
||||
|
||||
app_configs = {c.provider_type.lower(): c for c in ApplicationProviderConfig.query.filter_by(is_enabled=True).all()}
|
||||
app_configs = {c.provider_type.lower(): c for c in ApplicationProviderConfig.query.filter_by(is_enabled=True, deleted_at=None).all()}
|
||||
|
||||
user_orgs = g.current_user.get_organizations()
|
||||
org_configs = {}
|
||||
if user_orgs:
|
||||
organization_id = user_orgs[0].id
|
||||
org_level = ExternalProviderConfig.query.filter_by(organization_id=organization_id).all()
|
||||
org_level = ExternalProviderConfig.query.filter_by(organization_id=organization_id, deleted_at=None).all()
|
||||
org_configs = {c.provider_type.lower(): c for c in org_level}
|
||||
|
||||
def provider_info(provider_id, name):
|
||||
@@ -50,11 +50,11 @@ def get_provider_config(provider: str):
|
||||
return api_response(success=False, message="No organizations found for user", status=400, error_type="BAD_REQUEST")
|
||||
|
||||
organization_id = user_orgs[0].id
|
||||
member = OrganizationMember.query.filter_by(user_id=g.current_user.id, organization_id=organization_id).first()
|
||||
member = OrganizationMember.query.filter_by(user_id=g.current_user.id, organization_id=organization_id, deleted_at=None).first()
|
||||
if not member or member.role not in [OrganizationRole.OWNER, OrganizationRole.ADMIN]:
|
||||
return api_response(success=False, message="Admin access required", status=403, error_type="FORBIDDEN")
|
||||
|
||||
config = ExternalProviderConfig.query.filter_by(organization_id=organization_id, provider_type=provider_type.value).first()
|
||||
config = ExternalProviderConfig.query.filter_by(organization_id=organization_id, provider_type=provider_type.value, deleted_at=None).first()
|
||||
if not config:
|
||||
return api_response(success=False, message=f"{provider.title()} OAuth is not configured", status=404, error_type="NOT_FOUND")
|
||||
|
||||
@@ -74,7 +74,7 @@ def create_or_update_provider_config(provider: str):
|
||||
return api_response(success=False, message="No organizations found for user", status=400, error_type="BAD_REQUEST")
|
||||
|
||||
organization_id = user_orgs[0].id
|
||||
member = OrganizationMember.query.filter_by(user_id=g.current_user.id, organization_id=organization_id).first()
|
||||
member = OrganizationMember.query.filter_by(user_id=g.current_user.id, organization_id=organization_id, deleted_at=None).first()
|
||||
if not member or member.role not in [OrganizationRole.OWNER, OrganizationRole.ADMIN]:
|
||||
return api_response(success=False, message="Admin access required", status=403, error_type="FORBIDDEN")
|
||||
|
||||
@@ -85,7 +85,7 @@ def create_or_update_provider_config(provider: str):
|
||||
if not client_id:
|
||||
return api_response(success=False, message="client_id is required", status=400, error_type="VALIDATION_ERROR")
|
||||
|
||||
config = ExternalProviderConfig.query.filter_by(organization_id=organization_id, provider_type=provider_type.value).first()
|
||||
config = ExternalProviderConfig.query.filter_by(organization_id=organization_id, provider_type=provider_type.value, deleted_at=None).first()
|
||||
is_new = config is None
|
||||
|
||||
if config:
|
||||
@@ -137,11 +137,11 @@ def delete_provider_config(provider: str):
|
||||
return api_response(success=False, message="No organizations found for user", status=400, error_type="BAD_REQUEST")
|
||||
|
||||
organization_id = user_orgs[0].id
|
||||
member = OrganizationMember.query.filter_by(user_id=g.current_user.id, organization_id=organization_id).first()
|
||||
member = OrganizationMember.query.filter_by(user_id=g.current_user.id, organization_id=organization_id, deleted_at=None).first()
|
||||
if not member or member.role not in [OrganizationRole.OWNER, OrganizationRole.ADMIN]:
|
||||
return api_response(success=False, message="Admin access required", status=403, error_type="FORBIDDEN")
|
||||
|
||||
config = ExternalProviderConfig.query.filter_by(organization_id=organization_id, provider_type=provider_type.value).first()
|
||||
config = ExternalProviderConfig.query.filter_by(organization_id=organization_id, provider_type=provider_type.value, deleted_at=None).first()
|
||||
if not config:
|
||||
return api_response(success=False, message=f"{provider.title()} OAuth is not configured", status=404, error_type="NOT_FOUND")
|
||||
|
||||
|
||||
@@ -819,9 +819,9 @@ def oidc_register():
|
||||
|
||||
org_id = data.get("organization_id")
|
||||
if org_id:
|
||||
organization = Organization.query.get(org_id)
|
||||
organization = Organization.query.filter_by(id=org_id, deleted_at=None).first()
|
||||
else:
|
||||
organization = Organization.query.filter_by(is_active=True).first()
|
||||
organization = Organization.query.filter_by(is_active=True, deleted_at=None).first()
|
||||
|
||||
if not organization:
|
||||
organization = Organization(
|
||||
|
||||
@@ -158,7 +158,7 @@ def send_mfa_reminder(org_id, user_id):
|
||||
if not user:
|
||||
return api_response(success=False, message="User not found", status=404)
|
||||
|
||||
compliance = MfaPolicyCompliance.query.filter_by(user_id=user_id, organization_id=org_id).first()
|
||||
compliance = MfaPolicyCompliance.query.filter_by(user_id=user_id, organization_id=org_id, deleted_at=None).first()
|
||||
policy = OrganizationSecurityPolicy.query.filter_by(organization_id=org_id).first()
|
||||
|
||||
if compliance and policy and compliance.deadline_at:
|
||||
|
||||
@@ -68,7 +68,7 @@ def sign_certificate():
|
||||
)
|
||||
|
||||
allowed_principal_names = set()
|
||||
memberships = OrganizationMember.query.filter_by(user_id=user_id).all()
|
||||
memberships = OrganizationMember.query.filter_by(user_id=user_id, deleted_at=None).all()
|
||||
for om in memberships:
|
||||
org = om.organization
|
||||
if not org or org.deleted_at is not None:
|
||||
|
||||
@@ -374,7 +374,7 @@ class OAuthState(BaseModel):
|
||||
def cleanup_expired(cls) -> None:
|
||||
"""Remove expired OAuth states."""
|
||||
now = datetime.now(timezone.utc)
|
||||
cls.query.filter(cls.expires_at < now).delete()
|
||||
cls.query.filter(cls.expires_at < now).filter(cls.deleted_at == None).update({"deleted_at": now}, synchronize_session=False)
|
||||
db.session.commit()
|
||||
|
||||
def to_dict(self, exclude=None):
|
||||
|
||||
@@ -32,7 +32,8 @@ class EmailVerificationToken(BaseModel):
|
||||
|
||||
Any existing unused tokens for this user are invalidated first.
|
||||
"""
|
||||
cls.query.filter_by(user_id=user_id, used_at=None).delete()
|
||||
now = datetime.now(timezone.utc)
|
||||
cls.query.filter_by(user_id=user_id, used_at=None).filter(cls.deleted_at == None).update({"deleted_at": now}, synchronize_session=False)
|
||||
db.session.flush()
|
||||
|
||||
token_value = secrets.token_urlsafe(48)
|
||||
|
||||
@@ -33,7 +33,8 @@ class PasswordResetToken(BaseModel):
|
||||
Any existing unused tokens for this user are invalidated first.
|
||||
"""
|
||||
# Invalidate any existing unused tokens for this user
|
||||
cls.query.filter_by(user_id=user_id, used_at=None).delete()
|
||||
now = datetime.now(timezone.utc)
|
||||
cls.query.filter_by(user_id=user_id, used_at=None).filter(cls.deleted_at == None).update({"deleted_at": now}, synchronize_session=False)
|
||||
db.session.flush()
|
||||
|
||||
token_value = secrets.token_urlsafe(48)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user