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.
@@ -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
- # Implementation similar to create but with UPDATE query
899
- raise NotImplementedError("Update operation will be implemented")
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
- # Implementation with status change to 'deleted'
904
- raise NotImplementedError("Delete operation will be implemented")
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
- # Implementation with password hashing and audit
909
- raise NotImplementedError("Change password operation will be implemented")
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
- # Implementation with secure token generation
914
- raise NotImplementedError("Reset password operation will be implemented")
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
- # Implementation with status change to 'inactive'
919
- raise NotImplementedError("Deactivate operation will be implemented")
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
- # Implementation with status change to 'active'
924
- raise NotImplementedError("Activate operation will be implemented")
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
- # Implementation with status change from 'deleted'
929
- raise NotImplementedError("Restore operation will be implemented")
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
- # Implementation with advanced search features
934
- raise NotImplementedError("Search operation will be implemented")
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
- # Implementation with transaction support
939
- raise NotImplementedError("Bulk update operation will be implemented")
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
- # Implementation with transaction support
944
- raise NotImplementedError("Bulk delete operation will be implemented")
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)}")