kailash 0.4.2__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/middleware/database/repositories.py +3 -1
- kailash/nodes/admin/audit_log.py +364 -6
- kailash/nodes/admin/user_management.py +1006 -20
- kailash/nodes/api/http.py +95 -71
- kailash/nodes/base.py +281 -164
- kailash/nodes/base_async.py +30 -31
- kailash/nodes/data/async_sql.py +3 -22
- kailash/utils/resource_manager.py +420 -0
- kailash/workflow/builder.py +93 -10
- kailash/workflow/cyclic_runner.py +4 -25
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/METADATA +6 -4
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/RECORD +16 -15
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/WHEEL +0 -0
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/top_level.txt +0 -0
@@ -895,50 +895,1036 @@ class UserManagementNode(Node):
|
|
895
895
|
# Additional operations (update, delete, etc.) would follow similar patterns
|
896
896
|
def _update_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
897
897
|
"""Update user information."""
|
898
|
-
|
899
|
-
|
898
|
+
user_id = inputs.get("user_id")
|
899
|
+
user_data = inputs.get("user_data", {})
|
900
|
+
tenant_id = inputs.get("tenant_id", "default")
|
901
|
+
|
902
|
+
if not user_id:
|
903
|
+
raise NodeValidationError("user_id is required for update operation")
|
904
|
+
|
905
|
+
# Build update fields
|
906
|
+
update_fields = []
|
907
|
+
params = []
|
908
|
+
param_count = 1
|
909
|
+
|
910
|
+
# Update allowed fields
|
911
|
+
allowed_fields = [
|
912
|
+
"email",
|
913
|
+
"username",
|
914
|
+
"first_name",
|
915
|
+
"last_name",
|
916
|
+
"status",
|
917
|
+
"roles",
|
918
|
+
"attributes",
|
919
|
+
"phone",
|
920
|
+
"department",
|
921
|
+
]
|
922
|
+
|
923
|
+
for field, value in user_data.items():
|
924
|
+
if field in allowed_fields:
|
925
|
+
# Validate specific fields
|
926
|
+
if field == "email" and inputs.get("validate_email", True):
|
927
|
+
if not self._validate_email(value):
|
928
|
+
raise NodeValidationError(f"Invalid email format: {value}")
|
929
|
+
elif field == "username" and inputs.get("validate_username", True):
|
930
|
+
if not self._validate_username(value):
|
931
|
+
raise NodeValidationError(f"Invalid username format: {value}")
|
932
|
+
elif field == "status":
|
933
|
+
if value not in [s.value for s in UserStatus]:
|
934
|
+
raise NodeValidationError(f"Invalid status: {value}")
|
935
|
+
|
936
|
+
update_fields.append(f"{field} = ${param_count}")
|
937
|
+
params.append(value)
|
938
|
+
param_count += 1
|
939
|
+
|
940
|
+
if not update_fields:
|
941
|
+
return {"success": False, "message": "No valid fields to update"}
|
942
|
+
|
943
|
+
# Add updated_at
|
944
|
+
update_fields.append(f"updated_at = ${param_count}")
|
945
|
+
params.append(datetime.now(UTC))
|
946
|
+
param_count += 1
|
947
|
+
|
948
|
+
# Build query
|
949
|
+
update_query = f"""
|
950
|
+
UPDATE users
|
951
|
+
SET {', '.join(update_fields)}
|
952
|
+
WHERE user_id = ${param_count} AND tenant_id = ${param_count + 1}
|
953
|
+
RETURNING user_id, email, username, first_name, last_name, status, roles, attributes
|
954
|
+
"""
|
955
|
+
params.extend([user_id, tenant_id])
|
956
|
+
|
957
|
+
self._ensure_db_node(inputs)
|
958
|
+
result = self._db_node.execute(query=update_query, params=params)
|
959
|
+
|
960
|
+
if not result.get("rows"):
|
961
|
+
return {"success": False, "message": "User not found"}
|
962
|
+
|
963
|
+
updated_user = result["rows"][0]
|
964
|
+
|
965
|
+
# Audit log
|
966
|
+
if self._config.audit_enabled:
|
967
|
+
print(f"[AUDIT] user_updated: {user_id}")
|
968
|
+
|
969
|
+
return {
|
970
|
+
"success": True,
|
971
|
+
"user": updated_user,
|
972
|
+
"message": f"User {user_id} updated successfully",
|
973
|
+
}
|
900
974
|
|
901
975
|
def _delete_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
902
976
|
"""Soft delete user."""
|
903
|
-
|
904
|
-
|
977
|
+
user_id = inputs.get("user_id")
|
978
|
+
tenant_id = inputs.get("tenant_id", "default")
|
979
|
+
hard_delete = inputs.get("hard_delete", False)
|
980
|
+
|
981
|
+
if not user_id:
|
982
|
+
raise NodeValidationError("user_id is required for delete operation")
|
983
|
+
|
984
|
+
self._ensure_db_node(inputs)
|
985
|
+
|
986
|
+
if hard_delete:
|
987
|
+
# Permanent deletion - use with caution
|
988
|
+
delete_query = """
|
989
|
+
DELETE FROM users
|
990
|
+
WHERE user_id = $1 AND tenant_id = $2
|
991
|
+
RETURNING user_id, email, username
|
992
|
+
"""
|
993
|
+
else:
|
994
|
+
# Soft delete - change status to 'deleted'
|
995
|
+
delete_query = """
|
996
|
+
UPDATE users
|
997
|
+
SET status = 'deleted',
|
998
|
+
updated_at = $3,
|
999
|
+
deleted_at = $3,
|
1000
|
+
deleted_by = $4
|
1001
|
+
WHERE user_id = $1 AND tenant_id = $2 AND status != 'deleted'
|
1002
|
+
RETURNING user_id, email, username, status
|
1003
|
+
"""
|
1004
|
+
|
1005
|
+
params = [user_id, tenant_id]
|
1006
|
+
if not hard_delete:
|
1007
|
+
params.extend([datetime.now(UTC), inputs.get("deleted_by", "system")])
|
1008
|
+
|
1009
|
+
result = self._db_node.execute(query=delete_query, params=params)
|
1010
|
+
|
1011
|
+
if not result.get("rows"):
|
1012
|
+
return {"success": False, "message": "User not found or already deleted"}
|
1013
|
+
|
1014
|
+
deleted_user = result["rows"][0]
|
1015
|
+
|
1016
|
+
# Audit log
|
1017
|
+
if self._config.audit_enabled:
|
1018
|
+
action = "hard_deleted" if hard_delete else "soft_deleted"
|
1019
|
+
print(f"[AUDIT] user_{action}: {user_id}")
|
1020
|
+
|
1021
|
+
return {
|
1022
|
+
"success": True,
|
1023
|
+
"user": deleted_user,
|
1024
|
+
"message": f"User {user_id} deleted successfully",
|
1025
|
+
"hard_delete": hard_delete,
|
1026
|
+
}
|
905
1027
|
|
906
1028
|
def _change_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
907
1029
|
"""Change user password."""
|
908
|
-
|
909
|
-
|
1030
|
+
user_id = inputs.get("user_id")
|
1031
|
+
current_password = inputs.get("current_password")
|
1032
|
+
new_password = inputs.get("new_password")
|
1033
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1034
|
+
skip_current_check = inputs.get("skip_current_check", False)
|
1035
|
+
|
1036
|
+
if not user_id:
|
1037
|
+
raise NodeValidationError("user_id is required for password change")
|
1038
|
+
if not new_password:
|
1039
|
+
raise NodeValidationError("new_password is required")
|
1040
|
+
if not skip_current_check and not current_password:
|
1041
|
+
raise NodeValidationError(
|
1042
|
+
"current_password is required unless skip_current_check is True"
|
1043
|
+
)
|
1044
|
+
|
1045
|
+
self._ensure_db_node(inputs)
|
1046
|
+
|
1047
|
+
# Verify current password if required
|
1048
|
+
if not skip_current_check:
|
1049
|
+
verify_query = """
|
1050
|
+
SELECT password_hash
|
1051
|
+
FROM users
|
1052
|
+
WHERE user_id = $1 AND tenant_id = $2 AND status != 'deleted'
|
1053
|
+
"""
|
1054
|
+
result = self._db_node.execute(
|
1055
|
+
query=verify_query, params=[user_id, tenant_id]
|
1056
|
+
)
|
1057
|
+
|
1058
|
+
if not result.get("rows"):
|
1059
|
+
return {"success": False, "message": "User not found"}
|
1060
|
+
|
1061
|
+
stored_hash = result["rows"][0]["password_hash"]
|
1062
|
+
if stored_hash and not self._verify_password(current_password, stored_hash):
|
1063
|
+
return {"success": False, "message": "Current password is incorrect"}
|
1064
|
+
|
1065
|
+
# Validate new password against policy
|
1066
|
+
policy = self._config.password_policy
|
1067
|
+
if len(new_password) < policy["min_length"]:
|
1068
|
+
raise NodeValidationError(
|
1069
|
+
f"Password must be at least {policy['min_length']} characters"
|
1070
|
+
)
|
1071
|
+
|
1072
|
+
if policy.get("require_uppercase") and not any(
|
1073
|
+
c.isupper() for c in new_password
|
1074
|
+
):
|
1075
|
+
raise NodeValidationError("Password must contain uppercase letters")
|
1076
|
+
|
1077
|
+
if policy.get("require_lowercase") and not any(
|
1078
|
+
c.islower() for c in new_password
|
1079
|
+
):
|
1080
|
+
raise NodeValidationError("Password must contain lowercase letters")
|
1081
|
+
|
1082
|
+
if policy.get("require_numbers") and not any(c.isdigit() for c in new_password):
|
1083
|
+
raise NodeValidationError("Password must contain numbers")
|
1084
|
+
|
1085
|
+
if policy.get("require_special") and not any(
|
1086
|
+
c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in new_password
|
1087
|
+
):
|
1088
|
+
raise NodeValidationError("Password must contain special characters")
|
1089
|
+
|
1090
|
+
# Check password history if configured
|
1091
|
+
if policy.get("history_count", 0) > 0:
|
1092
|
+
history_query = """
|
1093
|
+
SELECT password_hash
|
1094
|
+
FROM password_history
|
1095
|
+
WHERE user_id = $1 AND tenant_id = $2
|
1096
|
+
ORDER BY created_at DESC
|
1097
|
+
LIMIT $3
|
1098
|
+
"""
|
1099
|
+
history_result = self._db_node.execute(
|
1100
|
+
query=history_query,
|
1101
|
+
params=[user_id, tenant_id, policy["history_count"]],
|
1102
|
+
)
|
1103
|
+
|
1104
|
+
for row in history_result.get("rows", []):
|
1105
|
+
if self._verify_password(new_password, row["password_hash"]):
|
1106
|
+
return {
|
1107
|
+
"success": False,
|
1108
|
+
"message": f"Password cannot be reused from last {policy['history_count']} passwords",
|
1109
|
+
}
|
1110
|
+
|
1111
|
+
# Hash new password
|
1112
|
+
new_hash = self._hash_password(new_password)
|
1113
|
+
|
1114
|
+
# Update password
|
1115
|
+
update_query = """
|
1116
|
+
UPDATE users
|
1117
|
+
SET password_hash = $1,
|
1118
|
+
password_changed_at = $2,
|
1119
|
+
updated_at = $2,
|
1120
|
+
force_password_change = false
|
1121
|
+
WHERE user_id = $3 AND tenant_id = $4
|
1122
|
+
RETURNING user_id, email, username
|
1123
|
+
"""
|
1124
|
+
|
1125
|
+
now = datetime.now(UTC)
|
1126
|
+
result = self._db_node.execute(
|
1127
|
+
query=update_query, params=[new_hash, now, user_id, tenant_id]
|
1128
|
+
)
|
1129
|
+
|
1130
|
+
if not result.get("rows"):
|
1131
|
+
return {"success": False, "message": "Failed to update password"}
|
1132
|
+
|
1133
|
+
# Store in password history
|
1134
|
+
if policy.get("history_count", 0) > 0:
|
1135
|
+
history_insert = """
|
1136
|
+
INSERT INTO password_history (user_id, tenant_id, password_hash, created_at)
|
1137
|
+
VALUES ($1, $2, $3, $4)
|
1138
|
+
"""
|
1139
|
+
self._db_node.execute(
|
1140
|
+
query=history_insert, params=[user_id, tenant_id, new_hash, now]
|
1141
|
+
)
|
1142
|
+
|
1143
|
+
# Audit log
|
1144
|
+
if self._config.audit_enabled:
|
1145
|
+
print(f"[AUDIT] password_changed: {user_id}")
|
1146
|
+
|
1147
|
+
return {
|
1148
|
+
"success": True,
|
1149
|
+
"user": result["rows"][0],
|
1150
|
+
"message": "Password changed successfully",
|
1151
|
+
}
|
910
1152
|
|
911
1153
|
def _reset_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
912
1154
|
"""Reset user password with token generation."""
|
913
|
-
|
914
|
-
|
1155
|
+
user_id = inputs.get("user_id")
|
1156
|
+
email = inputs.get("email")
|
1157
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1158
|
+
generate_token = inputs.get("generate_token", True)
|
1159
|
+
new_password = inputs.get("new_password")
|
1160
|
+
token_expiry_hours = inputs.get("token_expiry_hours", 24)
|
1161
|
+
|
1162
|
+
if not user_id and not email:
|
1163
|
+
raise NodeValidationError(
|
1164
|
+
"Either user_id or email is required for password reset"
|
1165
|
+
)
|
1166
|
+
|
1167
|
+
self._ensure_db_node(inputs)
|
1168
|
+
|
1169
|
+
# Find user
|
1170
|
+
if user_id:
|
1171
|
+
query = "SELECT user_id, email, username FROM users WHERE user_id = $1 AND tenant_id = $2 AND status != 'deleted'"
|
1172
|
+
params = [user_id, tenant_id]
|
1173
|
+
else:
|
1174
|
+
query = "SELECT user_id, email, username FROM users WHERE email = $1 AND tenant_id = $2 AND status != 'deleted'"
|
1175
|
+
params = [email, tenant_id]
|
1176
|
+
|
1177
|
+
result = self._db_node.execute(query=query, params=params)
|
1178
|
+
|
1179
|
+
if not result.get("rows"):
|
1180
|
+
return {"success": False, "message": "User not found"}
|
1181
|
+
|
1182
|
+
user_data = result["rows"][0]
|
1183
|
+
user_id = user_data["user_id"]
|
1184
|
+
|
1185
|
+
if generate_token:
|
1186
|
+
# Generate secure reset token
|
1187
|
+
reset_token = secrets.token_urlsafe(32)
|
1188
|
+
token_hash = hashlib.sha256(reset_token.encode()).hexdigest()
|
1189
|
+
expiry_time = datetime.now(UTC) + timedelta(hours=token_expiry_hours)
|
1190
|
+
|
1191
|
+
# Store reset token
|
1192
|
+
token_query = """
|
1193
|
+
INSERT INTO password_reset_tokens (user_id, tenant_id, token_hash, expires_at, created_at)
|
1194
|
+
VALUES ($1, $2, $3, $4, $5)
|
1195
|
+
ON CONFLICT (user_id, tenant_id)
|
1196
|
+
DO UPDATE SET token_hash = $3, expires_at = $4, created_at = $5, used = false
|
1197
|
+
"""
|
1198
|
+
|
1199
|
+
self._db_node.execute(
|
1200
|
+
query=token_query,
|
1201
|
+
params=[user_id, tenant_id, token_hash, expiry_time, datetime.now(UTC)],
|
1202
|
+
)
|
1203
|
+
|
1204
|
+
# Force password change on next login
|
1205
|
+
update_query = """
|
1206
|
+
UPDATE users
|
1207
|
+
SET force_password_change = true, updated_at = $1
|
1208
|
+
WHERE user_id = $2 AND tenant_id = $3
|
1209
|
+
"""
|
1210
|
+
self._db_node.execute(
|
1211
|
+
query=update_query, params=[datetime.now(UTC), user_id, tenant_id]
|
1212
|
+
)
|
1213
|
+
|
1214
|
+
# Audit log
|
1215
|
+
if self._config.audit_enabled:
|
1216
|
+
print(f"[AUDIT] password_reset_requested: {user_id}")
|
1217
|
+
|
1218
|
+
return {
|
1219
|
+
"success": True,
|
1220
|
+
"user": user_data,
|
1221
|
+
"reset_token": reset_token,
|
1222
|
+
"expires_at": expiry_time.isoformat(),
|
1223
|
+
"message": "Password reset token generated",
|
1224
|
+
}
|
1225
|
+
|
1226
|
+
elif new_password:
|
1227
|
+
# Direct password reset (admin action)
|
1228
|
+
# Validate new password
|
1229
|
+
policy = self._config.password_policy
|
1230
|
+
if len(new_password) < policy["min_length"]:
|
1231
|
+
raise NodeValidationError(
|
1232
|
+
f"Password must be at least {policy['min_length']} characters"
|
1233
|
+
)
|
1234
|
+
|
1235
|
+
# Hash and update password
|
1236
|
+
new_hash = self._hash_password(new_password)
|
1237
|
+
|
1238
|
+
update_query = """
|
1239
|
+
UPDATE users
|
1240
|
+
SET password_hash = $1,
|
1241
|
+
password_changed_at = $2,
|
1242
|
+
updated_at = $2,
|
1243
|
+
force_password_change = $3
|
1244
|
+
WHERE user_id = $4 AND tenant_id = $5
|
1245
|
+
RETURNING user_id, email, username
|
1246
|
+
"""
|
1247
|
+
|
1248
|
+
force_change = inputs.get("force_password_change", True)
|
1249
|
+
now = datetime.now(UTC)
|
1250
|
+
|
1251
|
+
result = self._db_node.execute(
|
1252
|
+
query=update_query,
|
1253
|
+
params=[new_hash, now, force_change, user_id, tenant_id],
|
1254
|
+
)
|
1255
|
+
|
1256
|
+
if not result.get("rows"):
|
1257
|
+
return {"success": False, "message": "Failed to reset password"}
|
1258
|
+
|
1259
|
+
# Audit log
|
1260
|
+
if self._config.audit_enabled:
|
1261
|
+
print(f"[AUDIT] password_reset_admin: {user_id}")
|
1262
|
+
|
1263
|
+
return {
|
1264
|
+
"success": True,
|
1265
|
+
"user": result["rows"][0],
|
1266
|
+
"message": "Password reset successfully",
|
1267
|
+
"force_password_change": force_change,
|
1268
|
+
}
|
1269
|
+
|
1270
|
+
else:
|
1271
|
+
raise NodeValidationError(
|
1272
|
+
"Either generate_token or new_password must be provided"
|
1273
|
+
)
|
915
1274
|
|
916
1275
|
def _deactivate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
917
1276
|
"""Deactivate user account."""
|
918
|
-
|
919
|
-
|
1277
|
+
user_id = inputs.get("user_id")
|
1278
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1279
|
+
reason = inputs.get("reason", "Manual deactivation")
|
1280
|
+
deactivated_by = inputs.get("deactivated_by", "system")
|
1281
|
+
|
1282
|
+
if not user_id:
|
1283
|
+
raise NodeValidationError("user_id is required for deactivate operation")
|
1284
|
+
|
1285
|
+
self._ensure_db_node(inputs)
|
1286
|
+
|
1287
|
+
# Update user status to inactive
|
1288
|
+
update_query = """
|
1289
|
+
UPDATE users
|
1290
|
+
SET status = 'inactive',
|
1291
|
+
updated_at = $1,
|
1292
|
+
deactivated_at = $1,
|
1293
|
+
deactivation_reason = $2,
|
1294
|
+
deactivated_by = $3
|
1295
|
+
WHERE user_id = $4 AND tenant_id = $5 AND status = 'active'
|
1296
|
+
RETURNING user_id, email, username, status, first_name, last_name
|
1297
|
+
"""
|
1298
|
+
|
1299
|
+
now = datetime.now(UTC)
|
1300
|
+
result = self._db_node.execute(
|
1301
|
+
query=update_query, params=[now, reason, deactivated_by, user_id, tenant_id]
|
1302
|
+
)
|
1303
|
+
|
1304
|
+
if not result.get("rows"):
|
1305
|
+
# Check if user exists but is already inactive
|
1306
|
+
check_query = """
|
1307
|
+
SELECT status FROM users
|
1308
|
+
WHERE user_id = $1 AND tenant_id = $2
|
1309
|
+
"""
|
1310
|
+
check_result = self._db_node.execute(
|
1311
|
+
query=check_query, params=[user_id, tenant_id]
|
1312
|
+
)
|
1313
|
+
|
1314
|
+
if check_result.get("rows"):
|
1315
|
+
current_status = check_result["rows"][0]["status"]
|
1316
|
+
return {
|
1317
|
+
"success": False,
|
1318
|
+
"message": f"User is already {current_status}",
|
1319
|
+
}
|
1320
|
+
else:
|
1321
|
+
return {"success": False, "message": "User not found"}
|
1322
|
+
|
1323
|
+
deactivated_user = result["rows"][0]
|
1324
|
+
|
1325
|
+
# Revoke active sessions
|
1326
|
+
session_query = """
|
1327
|
+
UPDATE user_sessions
|
1328
|
+
SET status = 'revoked', revoked_at = $1
|
1329
|
+
WHERE user_id = $2 AND tenant_id = $3 AND status = 'active'
|
1330
|
+
"""
|
1331
|
+
self._db_node.execute(query=session_query, params=[now, user_id, tenant_id])
|
1332
|
+
|
1333
|
+
# Audit log
|
1334
|
+
if self._config.audit_enabled:
|
1335
|
+
print(f"[AUDIT] user_deactivated: {user_id} (reason: {reason})")
|
1336
|
+
|
1337
|
+
return {
|
1338
|
+
"success": True,
|
1339
|
+
"user": deactivated_user,
|
1340
|
+
"message": f"User {user_id} deactivated successfully",
|
1341
|
+
"reason": reason,
|
1342
|
+
}
|
920
1343
|
|
921
1344
|
def _activate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
922
1345
|
"""Activate user account."""
|
923
|
-
|
924
|
-
|
1346
|
+
user_id = inputs.get("user_id")
|
1347
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1348
|
+
activated_by = inputs.get("activated_by", "system")
|
1349
|
+
clear_deactivation_data = inputs.get("clear_deactivation_data", True)
|
1350
|
+
|
1351
|
+
if not user_id:
|
1352
|
+
raise NodeValidationError("user_id is required for activate operation")
|
1353
|
+
|
1354
|
+
self._ensure_db_node(inputs)
|
1355
|
+
|
1356
|
+
# Update user status to active
|
1357
|
+
if clear_deactivation_data:
|
1358
|
+
update_query = """
|
1359
|
+
UPDATE users
|
1360
|
+
SET status = 'active',
|
1361
|
+
updated_at = $1,
|
1362
|
+
activated_at = $1,
|
1363
|
+
activated_by = $2,
|
1364
|
+
deactivated_at = NULL,
|
1365
|
+
deactivation_reason = NULL,
|
1366
|
+
deactivated_by = NULL
|
1367
|
+
WHERE user_id = $3 AND tenant_id = $4 AND status IN ('inactive', 'pending')
|
1368
|
+
RETURNING user_id, email, username, status, first_name, last_name
|
1369
|
+
"""
|
1370
|
+
else:
|
1371
|
+
update_query = """
|
1372
|
+
UPDATE users
|
1373
|
+
SET status = 'active',
|
1374
|
+
updated_at = $1,
|
1375
|
+
activated_at = $1,
|
1376
|
+
activated_by = $2
|
1377
|
+
WHERE user_id = $3 AND tenant_id = $4 AND status IN ('inactive', 'pending')
|
1378
|
+
RETURNING user_id, email, username, status, first_name, last_name
|
1379
|
+
"""
|
1380
|
+
|
1381
|
+
now = datetime.now(UTC)
|
1382
|
+
result = self._db_node.execute(
|
1383
|
+
query=update_query, params=[now, activated_by, user_id, tenant_id]
|
1384
|
+
)
|
1385
|
+
|
1386
|
+
if not result.get("rows"):
|
1387
|
+
# Check if user exists but is already active
|
1388
|
+
check_query = """
|
1389
|
+
SELECT status FROM users
|
1390
|
+
WHERE user_id = $1 AND tenant_id = $2
|
1391
|
+
"""
|
1392
|
+
check_result = self._db_node.execute(
|
1393
|
+
query=check_query, params=[user_id, tenant_id]
|
1394
|
+
)
|
1395
|
+
|
1396
|
+
if check_result.get("rows"):
|
1397
|
+
current_status = check_result["rows"][0]["status"]
|
1398
|
+
if current_status == "active":
|
1399
|
+
return {"success": False, "message": "User is already active"}
|
1400
|
+
elif current_status == "deleted":
|
1401
|
+
return {
|
1402
|
+
"success": False,
|
1403
|
+
"message": "Cannot activate deleted user. Use restore operation instead.",
|
1404
|
+
}
|
1405
|
+
else:
|
1406
|
+
return {
|
1407
|
+
"success": False,
|
1408
|
+
"message": f"Cannot activate user with status: {current_status}",
|
1409
|
+
}
|
1410
|
+
else:
|
1411
|
+
return {"success": False, "message": "User not found"}
|
1412
|
+
|
1413
|
+
activated_user = result["rows"][0]
|
1414
|
+
|
1415
|
+
# Audit log
|
1416
|
+
if self._config.audit_enabled:
|
1417
|
+
print(f"[AUDIT] user_activated: {user_id}")
|
1418
|
+
|
1419
|
+
return {
|
1420
|
+
"success": True,
|
1421
|
+
"user": activated_user,
|
1422
|
+
"message": f"User {user_id} activated successfully",
|
1423
|
+
}
|
1424
|
+
|
1425
|
+
def _ensure_db_node(self, inputs: Dict[str, Any]):
|
1426
|
+
"""Ensure database node is initialized."""
|
1427
|
+
if not self._db_node:
|
1428
|
+
self._init_dependencies(inputs)
|
1429
|
+
|
1430
|
+
def _verify_password(self, password: str, password_hash: str) -> bool:
|
1431
|
+
"""Verify password against hash."""
|
1432
|
+
if not password_hash or "$" not in password_hash:
|
1433
|
+
return False
|
1434
|
+
|
1435
|
+
salt, stored_hash = password_hash.split("$", 1)
|
1436
|
+
test_hash = hashlib.sha256((password + salt).encode("utf-8")).hexdigest()
|
1437
|
+
return test_hash == stored_hash
|
925
1438
|
|
926
1439
|
def _restore_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
927
1440
|
"""Restore soft-deleted user."""
|
928
|
-
|
929
|
-
|
1441
|
+
user_id = inputs.get("user_id")
|
1442
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1443
|
+
restored_by = inputs.get("restored_by", "system")
|
1444
|
+
new_status = inputs.get("new_status", "active")
|
1445
|
+
|
1446
|
+
if not user_id:
|
1447
|
+
raise NodeValidationError("user_id is required for restore operation")
|
1448
|
+
|
1449
|
+
if new_status not in ["active", "inactive", "pending"]:
|
1450
|
+
raise NodeValidationError(
|
1451
|
+
f"Invalid new_status: {new_status}. Must be active, inactive, or pending"
|
1452
|
+
)
|
1453
|
+
|
1454
|
+
self._ensure_db_node(inputs)
|
1455
|
+
|
1456
|
+
# Check if user exists and is deleted
|
1457
|
+
check_query = """
|
1458
|
+
SELECT user_id, email, username, status, deleted_at
|
1459
|
+
FROM users
|
1460
|
+
WHERE user_id = $1 AND tenant_id = $2
|
1461
|
+
"""
|
1462
|
+
check_result = self._db_node.execute(
|
1463
|
+
query=check_query, params=[user_id, tenant_id]
|
1464
|
+
)
|
1465
|
+
|
1466
|
+
if not check_result.get("rows"):
|
1467
|
+
return {"success": False, "message": "User not found"}
|
1468
|
+
|
1469
|
+
user_data = check_result["rows"][0]
|
1470
|
+
if user_data["status"] != "deleted":
|
1471
|
+
return {
|
1472
|
+
"success": False,
|
1473
|
+
"message": f"User is not deleted. Current status: {user_data['status']}",
|
1474
|
+
}
|
1475
|
+
|
1476
|
+
# Restore user
|
1477
|
+
restore_query = """
|
1478
|
+
UPDATE users
|
1479
|
+
SET status = $1,
|
1480
|
+
updated_at = $2,
|
1481
|
+
restored_at = $2,
|
1482
|
+
restored_by = $3,
|
1483
|
+
deleted_at = NULL,
|
1484
|
+
deleted_by = NULL
|
1485
|
+
WHERE user_id = $4 AND tenant_id = $5 AND status = 'deleted'
|
1486
|
+
RETURNING user_id, email, username, status, first_name, last_name
|
1487
|
+
"""
|
1488
|
+
|
1489
|
+
now = datetime.now(UTC)
|
1490
|
+
result = self._db_node.execute(
|
1491
|
+
query=restore_query,
|
1492
|
+
params=[new_status, now, restored_by, user_id, tenant_id],
|
1493
|
+
)
|
1494
|
+
|
1495
|
+
if not result.get("rows"):
|
1496
|
+
return {"success": False, "message": "Failed to restore user"}
|
1497
|
+
|
1498
|
+
restored_user = result["rows"][0]
|
1499
|
+
|
1500
|
+
# Audit log
|
1501
|
+
if self._config.audit_enabled:
|
1502
|
+
print(f"[AUDIT] user_restored: {user_id} (new_status: {new_status})")
|
1503
|
+
|
1504
|
+
return {
|
1505
|
+
"success": True,
|
1506
|
+
"user": restored_user,
|
1507
|
+
"message": f"User {user_id} restored successfully",
|
1508
|
+
"new_status": new_status,
|
1509
|
+
"previous_deleted_at": user_data["deleted_at"],
|
1510
|
+
}
|
930
1511
|
|
931
1512
|
def _search_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
932
1513
|
"""Advanced user search with full-text capabilities."""
|
933
|
-
|
934
|
-
|
1514
|
+
search_query = inputs.get("search_query", "")
|
1515
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1516
|
+
filters = inputs.get("filters", {})
|
1517
|
+
search_fields = inputs.get(
|
1518
|
+
"search_fields", ["email", "username", "first_name", "last_name"]
|
1519
|
+
)
|
1520
|
+
pagination = inputs.get(
|
1521
|
+
"pagination", {"page": 1, "size": 20, "sort": "relevance"}
|
1522
|
+
)
|
1523
|
+
include_deleted = inputs.get("include_deleted", False)
|
1524
|
+
fuzzy_search = inputs.get("fuzzy_search", True)
|
1525
|
+
|
1526
|
+
self._ensure_db_node(inputs)
|
1527
|
+
|
1528
|
+
# Build search conditions
|
1529
|
+
where_conditions = ["tenant_id = $1"]
|
1530
|
+
params = [tenant_id]
|
1531
|
+
param_count = 1
|
1532
|
+
|
1533
|
+
if not include_deleted:
|
1534
|
+
where_conditions.append("status != 'deleted'")
|
1535
|
+
|
1536
|
+
# Apply filters
|
1537
|
+
if "status" in filters:
|
1538
|
+
param_count += 1
|
1539
|
+
if isinstance(filters["status"], list):
|
1540
|
+
where_conditions.append(f"status = ANY(${param_count})")
|
1541
|
+
params.append(filters["status"])
|
1542
|
+
else:
|
1543
|
+
where_conditions.append(f"status = ${param_count}")
|
1544
|
+
params.append(filters["status"])
|
1545
|
+
|
1546
|
+
if "roles" in filters:
|
1547
|
+
param_count += 1
|
1548
|
+
where_conditions.append(f"roles && ${param_count}")
|
1549
|
+
params.append(filters["roles"])
|
1550
|
+
|
1551
|
+
if "created_after" in filters:
|
1552
|
+
param_count += 1
|
1553
|
+
where_conditions.append(f"created_at >= ${param_count}")
|
1554
|
+
params.append(filters["created_after"])
|
1555
|
+
|
1556
|
+
if "created_before" in filters:
|
1557
|
+
param_count += 1
|
1558
|
+
where_conditions.append(f"created_at <= ${param_count}")
|
1559
|
+
params.append(filters["created_before"])
|
1560
|
+
|
1561
|
+
# Apply attribute filters
|
1562
|
+
if "attributes" in filters:
|
1563
|
+
for attr_key, attr_value in filters["attributes"].items():
|
1564
|
+
param_count += 1
|
1565
|
+
where_conditions.append(f"attributes->>'{attr_key}' = ${param_count}")
|
1566
|
+
params.append(attr_value)
|
1567
|
+
|
1568
|
+
# Build search query
|
1569
|
+
if search_query:
|
1570
|
+
search_conditions = []
|
1571
|
+
param_count += 1
|
1572
|
+
|
1573
|
+
if fuzzy_search:
|
1574
|
+
# Use ILIKE for fuzzy matching
|
1575
|
+
search_pattern = f"%{search_query}%"
|
1576
|
+
params.append(search_pattern)
|
1577
|
+
|
1578
|
+
for field in search_fields:
|
1579
|
+
search_conditions.append(f"{field} ILIKE ${param_count}")
|
1580
|
+
else:
|
1581
|
+
# Exact match
|
1582
|
+
params.append(search_query)
|
1583
|
+
|
1584
|
+
for field in search_fields:
|
1585
|
+
search_conditions.append(f"{field} = ${param_count}")
|
1586
|
+
|
1587
|
+
if search_conditions:
|
1588
|
+
where_conditions.append(f"({' OR '.join(search_conditions)})")
|
1589
|
+
|
1590
|
+
# Get pagination settings
|
1591
|
+
page = pagination.get("page", 1)
|
1592
|
+
size = pagination.get("size", 20)
|
1593
|
+
sort_field = pagination.get("sort", "relevance")
|
1594
|
+
sort_direction = pagination.get("direction", "DESC")
|
1595
|
+
|
1596
|
+
# Calculate offset
|
1597
|
+
offset = (page - 1) * size
|
1598
|
+
|
1599
|
+
# Build relevance scoring for sorting
|
1600
|
+
if sort_field == "relevance" and search_query:
|
1601
|
+
relevance_score = f"""
|
1602
|
+
CASE
|
1603
|
+
WHEN email = ${param_count} THEN 4
|
1604
|
+
WHEN username = ${param_count} THEN 3
|
1605
|
+
WHEN email ILIKE ${param_count} THEN 2
|
1606
|
+
WHEN username ILIKE ${param_count} OR first_name ILIKE ${param_count} OR last_name ILIKE ${param_count} THEN 1
|
1607
|
+
ELSE 0
|
1608
|
+
END as relevance
|
1609
|
+
"""
|
1610
|
+
|
1611
|
+
order_by = "relevance DESC, created_at DESC"
|
1612
|
+
else:
|
1613
|
+
relevance_score = "0 as relevance"
|
1614
|
+
order_by = f"{sort_field} {sort_direction}"
|
1615
|
+
|
1616
|
+
# Count query
|
1617
|
+
count_query = f"""
|
1618
|
+
SELECT COUNT(*) as total
|
1619
|
+
FROM users
|
1620
|
+
WHERE {' AND '.join(where_conditions)}
|
1621
|
+
"""
|
1622
|
+
|
1623
|
+
# Data query
|
1624
|
+
data_query = f"""
|
1625
|
+
SELECT user_id, email, username, first_name, last_name,
|
1626
|
+
status, roles, attributes, created_at, updated_at, last_login,
|
1627
|
+
{relevance_score}
|
1628
|
+
FROM users
|
1629
|
+
WHERE {' AND '.join(where_conditions)}
|
1630
|
+
ORDER BY {order_by}
|
1631
|
+
LIMIT {size} OFFSET {offset}
|
1632
|
+
"""
|
1633
|
+
|
1634
|
+
# Execute count query
|
1635
|
+
count_result = self._db_node.execute(query=count_query, params=params)
|
1636
|
+
total_count = (
|
1637
|
+
count_result["rows"][0]["total"] if count_result.get("rows") else 0
|
1638
|
+
)
|
1639
|
+
|
1640
|
+
# Execute data query
|
1641
|
+
data_result = self._db_node.execute(query=data_query, params=params)
|
1642
|
+
users = data_result.get("rows", [])
|
1643
|
+
|
1644
|
+
# Calculate pagination info
|
1645
|
+
total_pages = (total_count + size - 1) // size if size > 0 else 0
|
1646
|
+
has_next = page < total_pages
|
1647
|
+
has_prev = page > 1
|
1648
|
+
|
1649
|
+
# Audit log search action
|
1650
|
+
if self._config.audit_enabled and search_query:
|
1651
|
+
print(f"[AUDIT] user_search: query='{search_query}', results={len(users)}")
|
1652
|
+
|
1653
|
+
return {
|
1654
|
+
"success": True,
|
1655
|
+
"users": users,
|
1656
|
+
"pagination": {
|
1657
|
+
"page": page,
|
1658
|
+
"size": size,
|
1659
|
+
"total": total_count,
|
1660
|
+
"total_pages": total_pages,
|
1661
|
+
"has_next": has_next,
|
1662
|
+
"has_prev": has_prev,
|
1663
|
+
},
|
1664
|
+
"search": {
|
1665
|
+
"query": search_query,
|
1666
|
+
"fields": search_fields,
|
1667
|
+
"fuzzy": fuzzy_search,
|
1668
|
+
},
|
1669
|
+
"filters_applied": filters,
|
1670
|
+
"message": f"Found {total_count} users matching criteria",
|
1671
|
+
}
|
935
1672
|
|
936
1673
|
def _bulk_update_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
937
1674
|
"""Bulk update multiple users."""
|
938
|
-
|
939
|
-
|
1675
|
+
user_updates = inputs.get("user_updates", [])
|
1676
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1677
|
+
updated_by = inputs.get("updated_by", "system")
|
1678
|
+
transaction_mode = inputs.get("transaction_mode", "all_or_none")
|
1679
|
+
|
1680
|
+
if not user_updates:
|
1681
|
+
raise NodeValidationError("user_updates list is required for bulk update")
|
1682
|
+
|
1683
|
+
if not isinstance(user_updates, list):
|
1684
|
+
raise NodeValidationError("user_updates must be a list")
|
1685
|
+
|
1686
|
+
self._ensure_db_node(inputs)
|
1687
|
+
|
1688
|
+
results = {"updated": [], "failed": [], "stats": {"updated": 0, "failed": 0}}
|
1689
|
+
|
1690
|
+
# Start transaction if all_or_none mode
|
1691
|
+
if transaction_mode == "all_or_none":
|
1692
|
+
self._db_node.execute(query="BEGIN")
|
1693
|
+
|
1694
|
+
try:
|
1695
|
+
for i, update_data in enumerate(user_updates):
|
1696
|
+
try:
|
1697
|
+
user_id = update_data.get("user_id")
|
1698
|
+
if not user_id:
|
1699
|
+
raise NodeValidationError(
|
1700
|
+
f"user_id missing in update at index {i}"
|
1701
|
+
)
|
1702
|
+
|
1703
|
+
# Build update fields
|
1704
|
+
update_fields = []
|
1705
|
+
params = []
|
1706
|
+
param_count = 1
|
1707
|
+
|
1708
|
+
# Update allowed fields
|
1709
|
+
allowed_fields = [
|
1710
|
+
"email",
|
1711
|
+
"username",
|
1712
|
+
"first_name",
|
1713
|
+
"last_name",
|
1714
|
+
"status",
|
1715
|
+
"roles",
|
1716
|
+
"attributes",
|
1717
|
+
"phone",
|
1718
|
+
"department",
|
1719
|
+
]
|
1720
|
+
|
1721
|
+
for field, value in update_data.items():
|
1722
|
+
if field in allowed_fields:
|
1723
|
+
# Validate specific fields
|
1724
|
+
if field == "email" and inputs.get("validate_email", True):
|
1725
|
+
if not self._validate_email(value):
|
1726
|
+
raise NodeValidationError(
|
1727
|
+
f"Invalid email format: {value}"
|
1728
|
+
)
|
1729
|
+
elif field == "username" and inputs.get(
|
1730
|
+
"validate_username", True
|
1731
|
+
):
|
1732
|
+
if not self._validate_username(value):
|
1733
|
+
raise NodeValidationError(
|
1734
|
+
f"Invalid username format: {value}"
|
1735
|
+
)
|
1736
|
+
elif field == "status":
|
1737
|
+
if value not in [s.value for s in UserStatus]:
|
1738
|
+
raise NodeValidationError(
|
1739
|
+
f"Invalid status: {value}"
|
1740
|
+
)
|
1741
|
+
|
1742
|
+
update_fields.append(f"{field} = ${param_count}")
|
1743
|
+
params.append(value)
|
1744
|
+
param_count += 1
|
1745
|
+
|
1746
|
+
if not update_fields:
|
1747
|
+
raise NodeValidationError(
|
1748
|
+
f"No valid fields to update at index {i}"
|
1749
|
+
)
|
1750
|
+
|
1751
|
+
# Add updated_at and updated_by
|
1752
|
+
update_fields.append(f"updated_at = ${param_count}")
|
1753
|
+
params.append(datetime.now(UTC))
|
1754
|
+
param_count += 1
|
1755
|
+
|
1756
|
+
update_fields.append(f"updated_by = ${param_count}")
|
1757
|
+
params.append(updated_by)
|
1758
|
+
param_count += 1
|
1759
|
+
|
1760
|
+
# Build query
|
1761
|
+
update_query = f"""
|
1762
|
+
UPDATE users
|
1763
|
+
SET {', '.join(update_fields)}
|
1764
|
+
WHERE user_id = ${param_count} AND tenant_id = ${param_count + 1}
|
1765
|
+
RETURNING user_id, email, username, status
|
1766
|
+
"""
|
1767
|
+
params.extend([user_id, tenant_id])
|
1768
|
+
|
1769
|
+
result = self._db_node.execute(query=update_query, params=params)
|
1770
|
+
|
1771
|
+
if result.get("rows"):
|
1772
|
+
results["updated"].append(
|
1773
|
+
{"index": i, "user": result["rows"][0]}
|
1774
|
+
)
|
1775
|
+
results["stats"]["updated"] += 1
|
1776
|
+
else:
|
1777
|
+
raise Exception("User not found or no changes made")
|
1778
|
+
|
1779
|
+
except Exception as e:
|
1780
|
+
error_info = {
|
1781
|
+
"index": i,
|
1782
|
+
"user_id": update_data.get("user_id"),
|
1783
|
+
"error": str(e),
|
1784
|
+
}
|
1785
|
+
|
1786
|
+
if transaction_mode == "all_or_none":
|
1787
|
+
# Rollback and return error
|
1788
|
+
self._db_node.execute(query="ROLLBACK")
|
1789
|
+
return {
|
1790
|
+
"success": False,
|
1791
|
+
"message": f"Bulk update failed at index {i}: {str(e)}",
|
1792
|
+
"error_detail": error_info,
|
1793
|
+
"stats": results["stats"],
|
1794
|
+
}
|
1795
|
+
else:
|
1796
|
+
# Continue with next update
|
1797
|
+
results["failed"].append(error_info)
|
1798
|
+
results["stats"]["failed"] += 1
|
1799
|
+
|
1800
|
+
# Commit transaction if all_or_none mode
|
1801
|
+
if transaction_mode == "all_or_none":
|
1802
|
+
self._db_node.execute(query="COMMIT")
|
1803
|
+
|
1804
|
+
# Audit log
|
1805
|
+
if self._config.audit_enabled:
|
1806
|
+
print(
|
1807
|
+
f"[AUDIT] bulk_user_update: updated={results['stats']['updated']}, failed={results['stats']['failed']}"
|
1808
|
+
)
|
1809
|
+
|
1810
|
+
return {
|
1811
|
+
"success": True,
|
1812
|
+
"results": results,
|
1813
|
+
"message": f"Bulk update completed: {results['stats']['updated']} updated, {results['stats']['failed']} failed",
|
1814
|
+
"transaction_mode": transaction_mode,
|
1815
|
+
}
|
1816
|
+
|
1817
|
+
except Exception as e:
|
1818
|
+
if transaction_mode == "all_or_none":
|
1819
|
+
self._db_node.execute(query="ROLLBACK")
|
1820
|
+
raise NodeExecutionError(f"Bulk update failed: {str(e)}")
|
940
1821
|
|
941
1822
|
def _bulk_delete_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
942
1823
|
"""Bulk delete multiple users."""
|
943
|
-
|
944
|
-
|
1824
|
+
user_ids = inputs.get("user_ids", [])
|
1825
|
+
tenant_id = inputs.get("tenant_id", "default")
|
1826
|
+
hard_delete = inputs.get("hard_delete", False)
|
1827
|
+
deleted_by = inputs.get("deleted_by", "system")
|
1828
|
+
transaction_mode = inputs.get("transaction_mode", "all_or_none")
|
1829
|
+
|
1830
|
+
if not user_ids:
|
1831
|
+
raise NodeValidationError("user_ids list is required for bulk delete")
|
1832
|
+
|
1833
|
+
if not isinstance(user_ids, list):
|
1834
|
+
raise NodeValidationError("user_ids must be a list")
|
1835
|
+
|
1836
|
+
self._ensure_db_node(inputs)
|
1837
|
+
|
1838
|
+
results = {"deleted": [], "failed": [], "stats": {"deleted": 0, "failed": 0}}
|
1839
|
+
|
1840
|
+
# Start transaction if all_or_none mode
|
1841
|
+
if transaction_mode == "all_or_none":
|
1842
|
+
self._db_node.execute(query="BEGIN")
|
1843
|
+
|
1844
|
+
try:
|
1845
|
+
now = datetime.now(UTC)
|
1846
|
+
|
1847
|
+
for i, user_id in enumerate(user_ids):
|
1848
|
+
try:
|
1849
|
+
if hard_delete:
|
1850
|
+
# Permanent deletion
|
1851
|
+
delete_query = """
|
1852
|
+
DELETE FROM users
|
1853
|
+
WHERE user_id = $1 AND tenant_id = $2
|
1854
|
+
RETURNING user_id, email, username
|
1855
|
+
"""
|
1856
|
+
params = [user_id, tenant_id]
|
1857
|
+
else:
|
1858
|
+
# Soft delete
|
1859
|
+
delete_query = """
|
1860
|
+
UPDATE users
|
1861
|
+
SET status = 'deleted',
|
1862
|
+
updated_at = $1,
|
1863
|
+
deleted_at = $1,
|
1864
|
+
deleted_by = $2
|
1865
|
+
WHERE user_id = $3 AND tenant_id = $4 AND status != 'deleted'
|
1866
|
+
RETURNING user_id, email, username, status
|
1867
|
+
"""
|
1868
|
+
params = [now, deleted_by, user_id, tenant_id]
|
1869
|
+
|
1870
|
+
result = self._db_node.execute(query=delete_query, params=params)
|
1871
|
+
|
1872
|
+
if result.get("rows"):
|
1873
|
+
results["deleted"].append(
|
1874
|
+
{"index": i, "user": result["rows"][0]}
|
1875
|
+
)
|
1876
|
+
results["stats"]["deleted"] += 1
|
1877
|
+
|
1878
|
+
# Revoke sessions for soft delete
|
1879
|
+
if not hard_delete:
|
1880
|
+
session_query = """
|
1881
|
+
UPDATE user_sessions
|
1882
|
+
SET status = 'revoked', revoked_at = $1
|
1883
|
+
WHERE user_id = $2 AND tenant_id = $3 AND status = 'active'
|
1884
|
+
"""
|
1885
|
+
self._db_node.execute(
|
1886
|
+
query=session_query, params=[now, user_id, tenant_id]
|
1887
|
+
)
|
1888
|
+
else:
|
1889
|
+
raise Exception("User not found or already deleted")
|
1890
|
+
|
1891
|
+
except Exception as e:
|
1892
|
+
error_info = {"index": i, "user_id": user_id, "error": str(e)}
|
1893
|
+
|
1894
|
+
if transaction_mode == "all_or_none":
|
1895
|
+
# Rollback and return error
|
1896
|
+
self._db_node.execute(query="ROLLBACK")
|
1897
|
+
return {
|
1898
|
+
"success": False,
|
1899
|
+
"message": f"Bulk delete failed at index {i}: {str(e)}",
|
1900
|
+
"error_detail": error_info,
|
1901
|
+
"stats": results["stats"],
|
1902
|
+
}
|
1903
|
+
else:
|
1904
|
+
# Continue with next deletion
|
1905
|
+
results["failed"].append(error_info)
|
1906
|
+
results["stats"]["failed"] += 1
|
1907
|
+
|
1908
|
+
# Commit transaction if all_or_none mode
|
1909
|
+
if transaction_mode == "all_or_none":
|
1910
|
+
self._db_node.execute(query="COMMIT")
|
1911
|
+
|
1912
|
+
# Audit log
|
1913
|
+
if self._config.audit_enabled:
|
1914
|
+
action = "hard_deleted" if hard_delete else "soft_deleted"
|
1915
|
+
print(
|
1916
|
+
f"[AUDIT] bulk_user_{action}: deleted={results['stats']['deleted']}, failed={results['stats']['failed']}"
|
1917
|
+
)
|
1918
|
+
|
1919
|
+
return {
|
1920
|
+
"success": True,
|
1921
|
+
"results": results,
|
1922
|
+
"message": f"Bulk delete completed: {results['stats']['deleted']} deleted, {results['stats']['failed']} failed",
|
1923
|
+
"hard_delete": hard_delete,
|
1924
|
+
"transaction_mode": transaction_mode,
|
1925
|
+
}
|
1926
|
+
|
1927
|
+
except Exception as e:
|
1928
|
+
if transaction_mode == "all_or_none":
|
1929
|
+
self._db_node.execute(query="ROLLBACK")
|
1930
|
+
raise NodeExecutionError(f"Bulk delete failed: {str(e)}")
|