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
+4 -6
View File
@@ -21,7 +21,7 @@ def admin_list_app_providers():
return api_response(success=False, message="Admin access required", status=403, error_type="FORBIDDEN") 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"}] 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 = [] result = []
for p in PROVIDERS: for p in PROVIDERS:
@@ -64,7 +64,7 @@ def admin_configure_app_provider(provider: str):
if not client_id: if not client_id:
return api_response(success=False, message="client_id is required", status=400, error_type="VALIDATION_ERROR") 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: if cfg:
cfg.client_id = client_id cfg.client_id = client_id
if client_secret: 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.auth.authentication_method import ApplicationProviderConfig
from gatehouse_app.models import OrganizationMember from gatehouse_app.models import OrganizationMember
from gatehouse_app.utils.constants import OrganizationRole from gatehouse_app.utils.constants import OrganizationRole
from gatehouse_app.extensions import db
admin_memberships = OrganizationMember.query.filter( admin_memberships = OrganizationMember.query.filter(
OrganizationMember.user_id == g.current_user.id, OrganizationMember.user_id == g.current_user.id,
@@ -100,10 +99,9 @@ def admin_delete_app_provider(provider: str):
if not admin_memberships: if not admin_memberships:
return api_response(success=False, message="Admin access required", status=403, error_type="FORBIDDEN") 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: if not cfg:
return api_response(success=False, message=f"Provider '{provider}' is not configured", status=404, error_type="NOT_FOUND") return api_response(success=False, message=f"Provider '{provider}' is not configured", status=404, error_type="NOT_FOUND")
db.session.delete(cfg) cfg.delete()
db.session.commit()
return api_response(message=f"{provider.capitalize()} OAuth provider configuration removed") return api_response(message=f"{provider.capitalize()} OAuth provider configuration removed")
+3 -2
View File
@@ -174,6 +174,7 @@ def select_organization():
auth_method = AuthenticationMethod.query.filter_by( auth_method = AuthenticationMethod.query.filter_by(
method_type=state_record.provider_type, method_type=state_record.provider_type,
deleted_at=None,
).order_by(AuthenticationMethod.created_at.desc()).first() ).order_by(AuthenticationMethod.created_at.desc()).first()
if not auth_method: if not auth_method:
@@ -181,11 +182,11 @@ def select_organization():
user = auth_method.user 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: if not org:
return api_response(success=False, message="Organization not found", status=404, error_type="NOT_FOUND") 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: if not member:
return api_response(success=False, message="You are not a member of this organization", status=403, error_type="FORBIDDEN") 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(): def list_providers():
from gatehouse_app.models.auth.authentication_method import ApplicationProviderConfig 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() user_orgs = g.current_user.get_organizations()
org_configs = {} org_configs = {}
if user_orgs: if user_orgs:
organization_id = user_orgs[0].id 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} org_configs = {c.provider_type.lower(): c for c in org_level}
def provider_info(provider_id, name): 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") return api_response(success=False, message="No organizations found for user", status=400, error_type="BAD_REQUEST")
organization_id = user_orgs[0].id 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]: 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") 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: if not config:
return api_response(success=False, message=f"{provider.title()} OAuth is not configured", status=404, error_type="NOT_FOUND") 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") return api_response(success=False, message="No organizations found for user", status=400, error_type="BAD_REQUEST")
organization_id = user_orgs[0].id 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]: 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") 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: if not client_id:
return api_response(success=False, message="client_id is required", status=400, error_type="VALIDATION_ERROR") 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 is_new = config is None
if config: 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") return api_response(success=False, message="No organizations found for user", status=400, error_type="BAD_REQUEST")
organization_id = user_orgs[0].id 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]: 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") 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: if not config:
return api_response(success=False, message=f"{provider.title()} OAuth is not configured", status=404, error_type="NOT_FOUND") return api_response(success=False, message=f"{provider.title()} OAuth is not configured", status=404, error_type="NOT_FOUND")
+2 -2
View File
@@ -819,9 +819,9 @@ def oidc_register():
org_id = data.get("organization_id") org_id = data.get("organization_id")
if org_id: if org_id:
organization = Organization.query.get(org_id) organization = Organization.query.filter_by(id=org_id, deleted_at=None).first()
else: 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: if not organization:
organization = Organization( organization = Organization(
@@ -158,7 +158,7 @@ def send_mfa_reminder(org_id, user_id):
if not user: if not user:
return api_response(success=False, message="User not found", status=404) 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() policy = OrganizationSecurityPolicy.query.filter_by(organization_id=org_id).first()
if compliance and policy and compliance.deadline_at: if compliance and policy and compliance.deadline_at:
+1 -1
View File
@@ -68,7 +68,7 @@ def sign_certificate():
) )
allowed_principal_names = set() 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: for om in memberships:
org = om.organization org = om.organization
if not org or org.deleted_at is not None: if not org or org.deleted_at is not None:
@@ -374,7 +374,7 @@ class OAuthState(BaseModel):
def cleanup_expired(cls) -> None: def cleanup_expired(cls) -> None:
"""Remove expired OAuth states.""" """Remove expired OAuth states."""
now = datetime.now(timezone.utc) 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() db.session.commit()
def to_dict(self, exclude=None): def to_dict(self, exclude=None):
@@ -32,7 +32,8 @@ class EmailVerificationToken(BaseModel):
Any existing unused tokens for this user are invalidated first. 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() db.session.flush()
token_value = secrets.token_urlsafe(48) token_value = secrets.token_urlsafe(48)
@@ -33,7 +33,8 @@ class PasswordResetToken(BaseModel):
Any existing unused tokens for this user are invalidated first. Any existing unused tokens for this user are invalidated first.
""" """
# Invalidate any existing unused tokens for this user # 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() db.session.flush()
token_value = secrets.token_urlsafe(48) token_value = secrets.token_urlsafe(48)
+5 -6
View File
@@ -36,9 +36,9 @@ class AuthService:
Raises: Raises:
EmailAlreadyExistsError: If email is already registered EmailAlreadyExistsError: If email is already registered
""" """
# Check if email already exists # Check if email already exists
existing_user = User.query.filter_by(email=email.lower()).first() existing_user = User.query.filter_by(email=email.lower(), deleted_at=None).first()
if existing_user and existing_user.deleted_at is None: if existing_user:
raise EmailAlreadyExistsError() raise EmailAlreadyExistsError()
# Create user # Create user
@@ -280,12 +280,11 @@ class AuthService:
raise ConflictError("TOTP is already enabled for this account") raise ConflictError("TOTP is already enabled for this account")
# Clean up any existing unverified TOTP enrollment attempts # 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() existing_totp_method = user.get_totp_method()
if existing_totp_method and not existing_totp_method.verified: if existing_totp_method and not existing_totp_method.verified:
logger.debug(f"Removing existing unverified TOTP method for user {user.id}") logger.debug(f"Removing existing unverified TOTP method for user {user.id}")
db.session.delete(existing_totp_method) # Hard delete - unverified methods are temporary existing_totp_method.delete(soft=True) # Soft delete - unverified methods are temporary
db.session.commit() # Commit to ensure deletion before creating new record
# Generate TOTP secret # Generate TOTP secret
secret = TOTPService.generate_secret() secret = TOTPService.generate_secret()
+6 -6
View File
@@ -50,8 +50,8 @@ class BillingService:
if not plan: if not plan:
raise ValueError("Plan not found") raise ValueError("Plan not found")
# Check if subscription already exists # Check if subscription already exists
existing = Subscription.query.filter_by(organization_id=organization_id).first() existing = Subscription.query.filter_by(organization_id=organization_id, deleted_at=None).first()
if existing: if existing:
raise ValueError("Organization already has a subscription") raise ValueError("Organization already has a subscription")
@@ -88,7 +88,7 @@ class BillingService:
Returns: Returns:
Updated subscription 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: if not subscription:
raise ValueError("No subscription found for organization") raise ValueError("No subscription found for organization")
@@ -111,7 +111,7 @@ class BillingService:
Returns: Returns:
Updated subscription 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: if not subscription:
raise ValueError("No subscription found for organization") raise ValueError("No subscription found for organization")
@@ -132,7 +132,7 @@ class BillingService:
Returns: Returns:
Updated subscription 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: if not subscription:
raise ValueError("No subscription found for organization") raise ValueError("No subscription found for organization")
@@ -158,7 +158,7 @@ class BillingService:
Returns: Returns:
Overage calculation with details 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: if not subscription:
return {"has_overage": False, "overage_cost": 0, "user_count": 0, "included_users": 0} 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 provider_type_str = provider_type.value if isinstance(provider_type, AuthMethodType) else provider_type
app_config = ApplicationProviderConfig.query.filter_by( app_config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type_str provider_type=provider_type_str, deleted_at=None
).first() ).first()
if not app_config: if not app_config:
@@ -64,6 +64,7 @@ class ExternalAuthService:
org_override_obj = OrganizationProviderOverride.query.filter_by( org_override_obj = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id, organization_id=organization_id,
provider_type=provider_type_str, provider_type=provider_type_str,
deleted_at=None,
).first() ).first()
if org_override_obj and not org_override_obj.is_enabled: if org_override_obj and not org_override_obj.is_enabled:
@@ -14,7 +14,7 @@ def create_app_provider_config(
**kwargs, **kwargs,
) -> ApplicationProviderConfig: ) -> ApplicationProviderConfig:
existing = ApplicationProviderConfig.query.filter_by( existing = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type provider_type=provider_type, deleted_at=None
).first() ).first()
if existing: if existing:
@@ -51,7 +51,7 @@ def update_app_provider_config(
**updates, **updates,
) -> ApplicationProviderConfig: ) -> ApplicationProviderConfig:
config = ApplicationProviderConfig.query.filter_by( config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type provider_type=provider_type, deleted_at=None
).first() ).first()
if not config: if not config:
@@ -90,7 +90,7 @@ def update_app_provider_config(
def get_app_provider_config(provider_type: str) -> ApplicationProviderConfig: def get_app_provider_config(provider_type: str) -> ApplicationProviderConfig:
config = ApplicationProviderConfig.query.filter_by( config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type provider_type=provider_type, deleted_at=None
).first() ).first()
if not config: if not config:
@@ -104,13 +104,13 @@ def get_app_provider_config(provider_type: str) -> ApplicationProviderConfig:
def list_app_provider_configs() -> list: 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] return [config.to_dict() for config in configs]
def delete_app_provider_config(provider_type: str) -> bool: def delete_app_provider_config(provider_type: str) -> bool:
config = ApplicationProviderConfig.query.filter_by( config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type provider_type=provider_type, deleted_at=None
).first() ).first()
if not config: if not config:
@@ -219,10 +219,11 @@ def authenticate_with_provider(
auth_method = AuthenticationMethod.query.filter_by( auth_method = AuthenticationMethod.query.filter_by(
method_type=provider_type, method_type=provider_type,
provider_user_id=user_info["provider_user_id"], provider_user_id=user_info["provider_user_id"],
deleted_at=None,
).first() ).first()
if not auth_method: 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: if existing_user:
AuditService.log_external_auth_login_failed( AuditService.log_external_auth_login_failed(
@@ -286,12 +287,13 @@ def unlink_provider(
auth_method = AuthenticationMethod.query.filter_by( auth_method = AuthenticationMethod.query.filter_by(
user_id=user_id, user_id=user_id,
method_type=provider_type, method_type=provider_type,
deleted_at=None,
).first() ).first()
if not auth_method: if not auth_method:
raise ExternalAuthError("Provider not linked", "PROVIDER_NOT_LINKED", 400) 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: if other_methods <= 1:
raise ExternalAuthError( raise ExternalAuthError(
"Cannot unlink the last authentication method", "Cannot unlink the last authentication method",
@@ -16,7 +16,7 @@ def create_org_provider_override(
**kwargs, **kwargs,
) -> OrganizationProviderOverride: ) -> OrganizationProviderOverride:
app_config = ApplicationProviderConfig.query.filter_by( app_config = ApplicationProviderConfig.query.filter_by(
provider_type=provider_type provider_type=provider_type, deleted_at=None
).first() ).first()
if not app_config: if not app_config:
@@ -29,6 +29,7 @@ def create_org_provider_override(
existing = OrganizationProviderOverride.query.filter_by( existing = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id, organization_id=organization_id,
provider_type=provider_type, provider_type=provider_type,
deleted_at=None,
).first() ).first()
if existing: if existing:
@@ -69,6 +70,7 @@ def update_org_provider_override(
override = OrganizationProviderOverride.query.filter_by( override = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id, organization_id=organization_id,
provider_type=provider_type, provider_type=provider_type,
deleted_at=None,
).first() ).first()
if not override: if not override:
@@ -110,6 +112,7 @@ def get_org_provider_override(
override = OrganizationProviderOverride.query.filter_by( override = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id, organization_id=organization_id,
provider_type=provider_type, provider_type=provider_type,
deleted_at=None,
).first() ).first()
if not override: if not override:
@@ -124,7 +127,7 @@ def get_org_provider_override(
def list_org_provider_overrides(organization_id: str) -> list: def list_org_provider_overrides(organization_id: str) -> list:
overrides = OrganizationProviderOverride.query.filter_by( overrides = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id organization_id=organization_id, deleted_at=None
).all() ).all()
return [override.to_dict() for override in overrides] 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( override = OrganizationProviderOverride.query.filter_by(
organization_id=organization_id, organization_id=organization_id,
provider_type=provider_type, provider_type=provider_type,
deleted_at=None,
).first() ).first()
if not override: if not override:
@@ -1024,17 +1024,17 @@ def revoke_membership_soft(
def hard_delete_membership(membership_id: str) -> None: 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 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( membership = DeviceNetworkMembership.query.filter(
DeviceNetworkMembership.id == membership_id, DeviceNetworkMembership.id == membership_id,
).first() ).first()
if not membership: 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 return
device = Device.query.get(membership.device_id) device = Device.query.get(membership.device_id)
@@ -1048,7 +1048,7 @@ def hard_delete_membership(membership_id: str) -> None:
except Exception as exc: except Exception as exc:
logger.warning(f"[hard_delete_membership] ZT delete failed for {device.node_id}: {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() db.session.commit()
AuditService.log_action( 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, "device_node_id": device.node_id if device else None,
"network_id": network.zerotier_network_id if network 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, success=True,
) )
@@ -111,7 +111,7 @@ def handle_register_callback(
access_token=tokens["access_token"], 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: if existing_user:
raise OAuthFlowError( raise OAuthFlowError(
f"An account with email {user_info['email']} already exists. " 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: if not device or not network:
logger.warning( logger.warning(
f"[Reconciliation] Membership {membership.id}: missing " 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() db.session.commit()
results["deleted"] += 1 results["deleted"] += 1
continue continue
@@ -304,20 +304,20 @@ def reconcile_deleted_memberships() -> dict:
except Exception as zt_exc: except Exception as zt_exc:
logger.warning( logger.warning(
f"[Reconciliation] ZT delete failed for node {node_id} " 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() db.session.commit()
results["deleted"] += 1 results["deleted"] += 1
logger.debug( logger.debug(
f"[Reconciliation] Hard-deleted membership {membership.id} " f"[Reconciliation] Soft-deleted membership {membership.id} "
f"(node={node_id}, network={network_label})." f"(node={node_id}, network={network_label})."
) )
except Exception as exc: except Exception as exc:
logger.error( 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, exc_info=True,
) )
results["errors"] += 1 results["errors"] += 1