mindroot 9.2.0__py3-none-any.whl → 9.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.
Files changed (186) hide show
  1. mindroot/coreplugins/admin/__init__.py +3 -1
  2. mindroot/coreplugins/admin/agent_router.py +250 -7
  3. mindroot/coreplugins/admin/asset_manager.py +164 -0
  4. mindroot/coreplugins/admin/command_router.py +236 -1
  5. mindroot/coreplugins/admin/mcp_catalog_routes.py +156 -0
  6. mindroot/coreplugins/admin/mcp_publish_routes.py +450 -0
  7. mindroot/coreplugins/admin/mcp_registry_routes.py +495 -0
  8. mindroot/coreplugins/admin/mcp_routes.py +216 -0
  9. mindroot/coreplugins/admin/mod.py +62 -0
  10. mindroot/coreplugins/admin/oauth_callback_router.py +84 -0
  11. mindroot/coreplugins/admin/persona_handler.py +15 -6
  12. mindroot/coreplugins/admin/persona_router.py +158 -2
  13. mindroot/coreplugins/admin/plugin_manager.py +63 -0
  14. mindroot/coreplugins/admin/plugin_router.py +1 -1
  15. mindroot/coreplugins/admin/plugin_router_fixed.py +23 -0
  16. mindroot/coreplugins/admin/plugin_router_new_not_working.py +145 -0
  17. mindroot/coreplugins/admin/plugin_routes.py +114 -0
  18. mindroot/coreplugins/admin/registry_settings_routes.py +140 -0
  19. mindroot/coreplugins/admin/router.py +116 -15
  20. mindroot/coreplugins/admin/service_models.py +1 -1
  21. mindroot/coreplugins/admin/settings_router.py +1 -0
  22. mindroot/coreplugins/admin/static/css/admin-custom.css +357 -2
  23. mindroot/coreplugins/admin/static/css/dark.css +1 -0
  24. mindroot/coreplugins/admin/static/css/default.css +4 -0
  25. mindroot/coreplugins/admin/static/js/about-info.js +367 -0
  26. mindroot/coreplugins/admin/static/js/agent-form.js +83 -3
  27. mindroot/coreplugins/admin/static/js/api-key-script.js +307 -0
  28. mindroot/coreplugins/admin/static/js/mcp-manager.js +348 -0
  29. mindroot/coreplugins/admin/static/js/mcp-publisher.js +780 -0
  30. mindroot/coreplugins/admin/static/js/persona-editor.js +34 -5
  31. mindroot/coreplugins/admin/static/js/plugin-toggle.js +1 -1
  32. mindroot/coreplugins/admin/static/js/recommended-plugin-install.js +63 -0
  33. mindroot/coreplugins/admin/static/js/registry-auth-section.js +132 -0
  34. mindroot/coreplugins/admin/static/js/registry-manager-base.js +613 -0
  35. mindroot/coreplugins/admin/static/js/registry-manager-old.js +385 -0
  36. mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
  37. mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
  38. mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
  39. mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
  40. mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
  41. mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
  42. mindroot/coreplugins/admin/static/js/registry-shared-services.js +857 -0
  43. mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
  44. mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
  45. mindroot/coreplugins/admin/static/logo.png +0 -0
  46. mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
  47. mindroot/coreplugins/agent/Assistant/agent.json +27 -11
  48. mindroot/coreplugins/agent/agent.py +2 -2
  49. mindroot/coreplugins/agent/command_parser.py +25 -10
  50. mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
  51. mindroot/coreplugins/chat/__init__.py +4 -1
  52. mindroot/coreplugins/chat/router.py +132 -20
  53. mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
  54. mindroot/coreplugins/chat/services.py +31 -1
  55. mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
  56. mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
  57. mindroot/coreplugins/chat/static/css/dark.css +24 -3
  58. mindroot/coreplugins/chat/static/css/default.css +24 -3
  59. mindroot/coreplugins/chat/static/css/main.css +1 -0
  60. mindroot/coreplugins/chat/static/js/action.js +137 -60
  61. mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
  62. mindroot/coreplugins/chat/static/js/chat.js +59 -16
  63. mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
  64. mindroot/coreplugins/chat/static/js/chatform.js +2 -2
  65. mindroot/coreplugins/chat/static/site.webmanifest +1 -1
  66. mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
  67. mindroot/coreplugins/chat/widget_manager.py +139 -0
  68. mindroot/coreplugins/chat/widget_routes.py +287 -0
  69. mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
  70. mindroot/coreplugins/email/__init__.py +2 -0
  71. mindroot/coreplugins/email/email_provider.py +2 -2
  72. mindroot/coreplugins/email/mod.py +100 -0
  73. mindroot/coreplugins/email/services.py +5 -3
  74. mindroot/coreplugins/email/smtp_handler.py +9 -3
  75. mindroot/coreplugins/email/test_email_service.py +75 -0
  76. mindroot/coreplugins/env_manager/mod.py +61 -25
  77. mindroot/coreplugins/home/router.py +37 -2
  78. mindroot/coreplugins/home/static/imgs/logo.png +0 -0
  79. mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
  80. mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
  81. mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
  82. mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
  83. mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
  84. mindroot/coreplugins/home/templates/home.jinja2 +15 -6
  85. mindroot/coreplugins/index/handlers/plugin_ops.py +1 -1
  86. mindroot/coreplugins/index/indices/default/index.json +6 -6
  87. mindroot/coreplugins/jwt_auth/middleware.py +47 -1
  88. mindroot/coreplugins/jwt_auth/mod.py +40 -17
  89. mindroot/coreplugins/l8n/__init__.py +6 -0
  90. mindroot/coreplugins/l8n/debug_loader.py +85 -0
  91. mindroot/coreplugins/l8n/debug_middleware.py +74 -0
  92. mindroot/coreplugins/l8n/l8n_constants.py +19 -0
  93. mindroot/coreplugins/l8n/language_detection.py +183 -0
  94. mindroot/coreplugins/l8n/middleware.py +151 -0
  95. mindroot/coreplugins/l8n/mod.py +277 -0
  96. mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
  97. mindroot/coreplugins/l8n/test_enhanced.py +298 -0
  98. mindroot/coreplugins/l8n/test_l8n.py +95 -0
  99. mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
  100. mindroot/coreplugins/l8n/test_middleware.py +272 -0
  101. mindroot/coreplugins/l8n/utils.py +232 -0
  102. mindroot/coreplugins/mcp_/__init__.py +14 -0
  103. mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
  104. mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
  105. mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
  106. mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
  107. mindroot/coreplugins/mcp_/mod.py +367 -0
  108. mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
  109. mindroot/coreplugins/mcp_/server_installer.py +79 -0
  110. mindroot/coreplugins/mcp_/setup.py +26 -0
  111. mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
  112. mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
  113. mindroot/coreplugins/persona/mod.py +12 -7
  114. mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
  115. mindroot/coreplugins/subscriptions/__init__.py +1 -0
  116. mindroot/coreplugins/subscriptions/mod.py +14 -3
  117. mindroot/coreplugins/subscriptions/router.py +3 -0
  118. mindroot/coreplugins/user_service/__init__.py +1 -2
  119. mindroot/coreplugins/user_service/admin_init.py +1 -0
  120. mindroot/coreplugins/user_service/email_service.py +72 -17
  121. mindroot/coreplugins/user_service/mod.py +10 -2
  122. mindroot/coreplugins/user_service/password_reset_service.py +180 -27
  123. mindroot/coreplugins/user_service/router.py +84 -22
  124. mindroot/lib/auth/api_key.py +28 -0
  125. mindroot/lib/cli/plugins.py +94 -0
  126. mindroot/lib/plugins/default_plugin_manifest.json +20 -0
  127. mindroot/lib/plugins/installation.py +5 -5
  128. mindroot/lib/plugins/l8n_static_handler.py +225 -0
  129. mindroot/lib/plugins/loader.py +33 -3
  130. mindroot/lib/plugins/loader_with_l8n.py +281 -0
  131. mindroot/lib/plugins/manifest.py +238 -17
  132. mindroot/lib/providers/commands.py +3 -1
  133. mindroot/lib/route_decorators.py +5 -5
  134. mindroot/lib/templates.py +183 -11
  135. mindroot/lib/utils/merge_arrays.py +1 -1
  136. mindroot/migrate.py +49 -0
  137. mindroot/registry/data_access.py +1 -1
  138. mindroot/server.py +47 -13
  139. mindroot/server_missing_normal_args.py +197 -0
  140. mindroot/server_prev.py +173 -0
  141. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/METADATA +7 -2
  142. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/RECORD +147 -114
  143. mindroot/coreplugins/admin/static/favicon/about.txt +0 -6
  144. mindroot/coreplugins/admin/static/favicon/android-chrome-512x512.png +0 -0
  145. mindroot/coreplugins/admin/static/favicon/apple-touch-icon.png +0 -0
  146. mindroot/coreplugins/admin/static/favicon/favicon-16x16.png +0 -0
  147. mindroot/coreplugins/admin/static/favicon/favicon-32x32.png +0 -0
  148. mindroot/coreplugins/admin/static/favicon/favicon.ico +0 -0
  149. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/about.txt +0 -6
  150. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  151. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  152. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  153. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  154. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  155. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon.ico +0 -0
  156. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  157. mindroot/coreplugins/admin/static/favicon/logo.png +0 -0
  158. mindroot/coreplugins/admin/static/favicon/site.webmanifest +0 -1
  159. mindroot/coreplugins/admin/static/js/backup/agent-editor.js +0 -186
  160. mindroot/coreplugins/admin/static/js/backup/agent-form.js +0 -1133
  161. mindroot/coreplugins/admin/static/js/backup/agent-list.js +0 -94
  162. mindroot/coreplugins/chat/static/favicon/about.txt +0 -6
  163. mindroot/coreplugins/chat/static/favicon/android-chrome-192x192.png +0 -0
  164. mindroot/coreplugins/chat/static/favicon/android-chrome-512x512.png +0 -0
  165. mindroot/coreplugins/chat/static/favicon/apple-touch-icon.png +0 -0
  166. mindroot/coreplugins/chat/static/favicon/favicon-16x16.png +0 -0
  167. mindroot/coreplugins/chat/static/favicon/favicon-32x32.png +0 -0
  168. mindroot/coreplugins/chat/static/favicon/favicon.ico +0 -0
  169. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/about.txt +0 -6
  170. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  171. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  172. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  173. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  174. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  175. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon.ico +0 -0
  176. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  177. mindroot/coreplugins/chat/static/favicon/logo.png +0 -0
  178. mindroot/coreplugins/chat/static/favicon/site.webmanifest +0 -1
  179. mindroot/coreplugins/index/default.json +0 -76
  180. mindroot/coreplugins/user_service/file_trigger_service.py +0 -72
  181. mindroot/coreplugins/user_service/hooks.py +0 -23
  182. /mindroot/coreplugins/{admin/static/favicon/android-chrome-192x192.png → home/static/imgs/backuplogo.png} +0 -0
  183. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/WHEEL +0 -0
  184. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/entry_points.txt +0 -0
  185. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/licenses/LICENSE +0 -0
  186. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ from typing import Optional
1
2
  from lib.providers.services import service
2
3
  from .models import UserAuth, PasswordResetToken
3
4
  import bcrypt
@@ -5,19 +6,47 @@ import json
5
6
  import os
6
7
  import secrets
7
8
  from datetime import datetime, timedelta
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
8
12
 
9
13
  USER_DATA_ROOT = "data/users"
10
14
  RESET_TOKEN_VALIDITY_HOURS = 1
11
15
 
12
16
  @service()
13
- async def initiate_password_reset(username: str, is_admin_reset: bool = False, context=None) -> str:
17
+ async def initiate_password_reset(username: str, is_admin_reset: bool = False, token: Optional[str] = None, context=None) -> str:
14
18
  """Initiates a password reset, generates a token, and stores it."""
19
+ # Log current working directory and absolute paths
20
+ cwd = os.getcwd()
21
+ abs_user_data_root = os.path.abspath(USER_DATA_ROOT)
15
22
  user_dir = os.path.join(USER_DATA_ROOT, username)
23
+ abs_user_dir = os.path.abspath(user_dir)
24
+
25
+ logger.info(f"=== PASSWORD RESET INITIATION ===")
26
+ logger.info(f"Current working directory: {cwd}")
27
+ logger.info(f"USER_DATA_ROOT (relative): {USER_DATA_ROOT}")
28
+ logger.info(f"USER_DATA_ROOT (absolute): {abs_user_data_root}")
29
+ logger.info(f"User directory (relative): {user_dir}")
30
+ logger.info(f"User directory (absolute): {abs_user_dir}")
31
+ logger.info(f"Initiating password reset for user: {username}")
32
+
16
33
  if not os.path.exists(user_dir):
34
+ logger.error(f"User directory not found: {user_dir} (absolute: {abs_user_dir})")
35
+ logger.error(f"Directory exists check: {os.path.exists(abs_user_dir)}")
17
36
  raise ValueError("User not found")
18
37
 
19
- token = secrets.token_urlsafe(32)
38
+ # List existing files in user directory
39
+ try:
40
+ existing_files = os.listdir(user_dir)
41
+ logger.info(f"Existing files in user directory: {existing_files}")
42
+ except Exception as e:
43
+ logger.error(f"Error listing user directory: {e}")
44
+
45
+ if token is None:
46
+ token = secrets.token_urlsafe(32)
47
+
20
48
  expires_at = datetime.utcnow() + timedelta(hours=RESET_TOKEN_VALIDITY_HOURS)
49
+ logger.info(f"Generated reset token for {username}, expires at: {expires_at.isoformat()}")
21
50
 
22
51
  reset_data = PasswordResetToken(
23
52
  token=token,
@@ -26,54 +55,178 @@ async def initiate_password_reset(username: str, is_admin_reset: bool = False, c
26
55
  )
27
56
 
28
57
  reset_file_path = os.path.join(user_dir, "password_reset.json")
58
+ abs_reset_file_path = os.path.abspath(reset_file_path)
59
+ logger.info(f"Saving reset token to: {reset_file_path} (absolute: {abs_reset_file_path})")
60
+
29
61
  with open(reset_file_path, 'w') as f:
30
62
  json.dump(reset_data.dict(), f, indent=2)
31
-
32
- # In a real application, you would email this token to the user.
33
- # For this implementation, we return it directly.
63
+
64
+ logger.info(f"Reset token file created successfully")
65
+
66
+ # Verify file was created
67
+ if os.path.exists(reset_file_path):
68
+ logger.info(f"Verified: Reset token file exists after creation")
69
+ try:
70
+ with open(reset_file_path, 'r') as f:
71
+ verify_data = json.load(f)
72
+ logger.info(f"Verified: Reset token file contents: {verify_data}")
73
+ except Exception as e:
74
+ logger.error(f"Error reading back reset token file: {e}")
75
+ else:
76
+ logger.error(f"ERROR: Reset token file was not created successfully!")
77
+
34
78
  return token
35
79
 
36
80
  @service()
37
81
  async def reset_password_with_token(token: str, new_password: str, context=None) -> bool:
38
82
  """Resets a user's password using a valid reset token."""
39
- for username in os.listdir(USER_DATA_ROOT):
83
+ # Log current working directory and absolute paths
84
+ cwd = os.getcwd()
85
+ abs_user_data_root = os.path.abspath(USER_DATA_ROOT)
86
+
87
+ logger.info(f"=== PASSWORD RESET ATTEMPT ===")
88
+ logger.info(f"Current working directory: {cwd}")
89
+ logger.info(f"USER_DATA_ROOT (relative): {USER_DATA_ROOT}")
90
+ logger.info(f"USER_DATA_ROOT (absolute): {abs_user_data_root}")
91
+ logger.info(f"Attempting password reset with token: {token[:10]}...{token[-10:]}")
92
+ logger.info(f"Token length: {len(token)}")
93
+
94
+ if not os.path.exists(USER_DATA_ROOT):
95
+ logger.error(f"USER_DATA_ROOT directory does not exist:")
96
+ logger.error(f" Relative path: {USER_DATA_ROOT}")
97
+ logger.error(f" Absolute path: {abs_user_data_root}")
98
+ logger.error(f" Exists check: {os.path.exists(abs_user_data_root)}")
99
+ raise ValueError("User data directory not found")
100
+
101
+ users_found = 0
102
+ tokens_checked = 0
103
+
104
+ try:
105
+ user_dirs = os.listdir(USER_DATA_ROOT)
106
+ logger.info(f"Found {len(user_dirs)} potential user directories: {user_dirs}")
107
+ except Exception as e:
108
+ logger.error(f"Error listing USER_DATA_ROOT: {e}")
109
+ raise ValueError("Error accessing user data")
110
+
111
+ for username in user_dirs:
40
112
  user_dir = os.path.join(USER_DATA_ROOT, username)
113
+ abs_user_dir = os.path.abspath(user_dir)
114
+ logger.info(f"Checking user directory: {user_dir} (absolute: {abs_user_dir})")
115
+
116
+ if not os.path.isdir(user_dir):
117
+ logger.debug(f"Skipping {username} - not a directory")
118
+ continue
119
+
120
+ users_found += 1
121
+
122
+ # List all files in this user directory
123
+ try:
124
+ user_files = os.listdir(user_dir)
125
+ logger.info(f"Files in user '{username}' directory: {user_files}")
126
+ except Exception as e:
127
+ logger.error(f"Error listing files in user directory {username}: {e}")
128
+ continue
129
+
41
130
  reset_file_path = os.path.join(user_dir, "password_reset.json")
131
+ abs_reset_file_path = os.path.abspath(reset_file_path)
132
+ logger.info(f"Looking for reset file: {reset_file_path} (absolute: {abs_reset_file_path})")
133
+ logger.info(f"Reset file exists: {os.path.exists(reset_file_path)}")
42
134
 
43
135
  if not os.path.exists(reset_file_path):
136
+ logger.info(f"No reset file found for user {username} - skipping")
137
+ continue
138
+
139
+ logger.info(f"Found reset file for user {username}, reading token data")
140
+
141
+ try:
142
+ with open(reset_file_path, 'r') as f:
143
+ reset_data_dict = json.load(f)
144
+ logger.info(f"Reset file contents for {username}: {reset_data_dict}")
145
+ reset_data = PasswordResetToken(**reset_data_dict)
146
+ except (json.JSONDecodeError, TypeError, ValueError) as e:
147
+ logger.error(f"Error parsing reset file for {username}: {e}")
148
+ continue
149
+ except Exception as e:
150
+ logger.error(f"Unexpected error reading reset file for {username}: {e}")
44
151
  continue
45
152
 
46
- with open(reset_file_path, 'r') as f:
153
+ tokens_checked += 1
154
+ stored_token = reset_data.token
155
+ logger.info(f"Comparing tokens for user {username}:")
156
+ logger.info(f" Provided token: '{token[:10]}...{token[-10:]}' (length: {len(token)})")
157
+ logger.info(f" Stored token: '{stored_token[:10]}...{stored_token[-10:]}' (length: {len(stored_token)})")
158
+ logger.info(f" Tokens match: {stored_token == token}")
159
+
160
+ # Also check for URL encoding issues
161
+ import urllib.parse
162
+ decoded_token = urllib.parse.unquote(token)
163
+ logger.info(f" URL decoded token: '{decoded_token[:10]}...{decoded_token[-10:]}' (length: {len(decoded_token)})")
164
+ logger.info(f" Decoded tokens match: {stored_token == decoded_token}")
165
+
166
+ if reset_data.token == token or reset_data.token == decoded_token:
167
+ logger.info(f"Token match found for user {username}!")
168
+ matched_token = token if reset_data.token == token else decoded_token
169
+ logger.info(f"Matched using {'original' if matched_token == token else 'URL-decoded'} token")
170
+
171
+ # Check expiration
47
172
  try:
48
- reset_data = PasswordResetToken(**json.load(f))
49
- except (json.JSONDecodeError, TypeError):
173
+ expires_at = datetime.fromisoformat(reset_data.expires_at)
174
+ current_time = datetime.utcnow()
175
+ logger.info(f"Token expires at: {expires_at}")
176
+ logger.info(f"Current time: {current_time}")
177
+ logger.info(f"Token expired: {expires_at < current_time}")
178
+
179
+ if expires_at < current_time:
180
+ logger.warning(f"Token expired for user {username}, removing reset file")
181
+ os.remove(reset_file_path)
182
+ raise ValueError("Password reset token has expired.")
183
+ except ValueError as e:
184
+ if "expired" in str(e):
185
+ raise
186
+ logger.error(f"Error parsing expiration date for {username}: {e}")
50
187
  continue
51
188
 
52
- if reset_data.token == token:
53
- if datetime.fromisoformat(reset_data.expires_at) < datetime.utcnow():
54
- os.remove(reset_file_path) # Expired token
55
- raise ValueError("Password reset token has expired.")
56
-
189
+ # Update password
57
190
  auth_file_path = os.path.join(user_dir, "auth.json")
191
+ abs_auth_file_path = os.path.abspath(auth_file_path)
192
+ logger.info(f"Updating password for user {username}")
193
+ logger.info(f"Auth file: {auth_file_path} (absolute: {abs_auth_file_path})")
194
+
58
195
  if not os.path.exists(auth_file_path):
196
+ logger.error(f"Auth file not found for user {username}: {auth_file_path}")
59
197
  os.remove(reset_file_path)
60
198
  raise FileNotFoundError("User auth file not found.")
61
199
 
62
- with open(auth_file_path, 'r+') as auth_file:
63
- auth_data_dict = json.load(auth_file)
64
- auth_data = UserAuth(**auth_data_dict)
65
-
66
- new_password_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode()
67
- auth_data.password_hash = new_password_hash
200
+ try:
201
+ with open(auth_file_path, 'r+') as auth_file:
202
+ auth_data_dict = json.load(auth_file)
203
+ auth_data = UserAuth(**auth_data_dict)
68
204
 
69
- if reset_data.is_admin_reset and 'admin' not in auth_data.roles:
70
- auth_data.roles.append('admin')
205
+ new_password_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode()
206
+ auth_data.password_hash = new_password_hash
207
+ logger.info(f"Password hash updated for user {username}")
71
208
 
72
- auth_file.seek(0)
73
- json.dump(auth_data.dict(), auth_file, indent=2, default=str)
74
- auth_file.truncate()
209
+ if reset_data.is_admin_reset and 'admin' not in auth_data.roles:
210
+ auth_data.roles.append('admin')
211
+ logger.info(f"Added admin role to user {username}")
75
212
 
76
- os.remove(reset_file_path) # Invalidate token after use
77
- return True
213
+ auth_file.seek(0)
214
+ json.dump(auth_data.dict(), auth_file, indent=2, default=str)
215
+ auth_file.truncate()
216
+ logger.info(f"Auth file updated for user {username}")
78
217
 
218
+ os.remove(reset_file_path)
219
+ logger.info(f"Reset token file removed for user {username}")
220
+ logger.info(f"Password reset completed successfully for user {username}")
221
+ return True
222
+
223
+ except Exception as e:
224
+ logger.error(f"Error updating auth file for user {username}: {e}")
225
+ raise ValueError(f"Error updating user authentication: {e}")
226
+
227
+ logger.warning(f"=== TOKEN NOT FOUND ===")
228
+ logger.warning(f"Token not found after checking {users_found} users and {tokens_checked} tokens")
229
+ logger.warning(f"Provided token: '{token}'")
230
+ logger.warning(f"Current working directory: {cwd}")
231
+ logger.warning(f"Searched in: {abs_user_data_root}")
79
232
  raise ValueError("Invalid password reset token.")
@@ -1,36 +1,98 @@
1
1
  from fastapi import APIRouter, Request, Form, Depends
2
2
  from fastapi.responses import HTMLResponse, RedirectResponse
3
3
  from lib.templates import render
4
+ import os
5
+ import json
6
+ import logging
7
+ # Import services directly to avoid coroutine-call confusion
4
8
  from .password_reset_service import reset_password_with_token, initiate_password_reset
5
9
  from lib.providers.services import service_manager
6
10
  from lib.providers import ProviderManager
11
+ from lib.route_decorators import public_route
7
12
 
13
+ logger = logging.getLogger(__name__)
8
14
  router = APIRouter()
9
15
 
10
- @router.get("/reset-password/{token}")
11
- async def get_reset_password_form(request: Request, token: str):
12
- return await render('reset_password', {"request": request, "token": token, "error": None, "success": False})
16
+ @public_route()
17
+ @router.get("/reset-password/{filename}")
18
+ async def get_reset_password_form_by_file(request: Request, filename: str):
19
+ """Show password reset form if trigger file exists"""
20
+ # print the current working directory
21
+ print("Current working directory:", os.getcwd())
22
+ trigger_dir = "data/password_resets"
23
+ file_path = os.path.join(trigger_dir, f"{filename}")
24
+ print("file path", file_path)
13
25
 
14
- @router.post("/reset-password/{token}")
15
- async def handle_reset_password(request: Request, token: str, password: str = Form(...), confirm_password: str = Form(...), services: ProviderManager = Depends(lambda: service_manager)):
16
- if password != confirm_password:
17
- return await render('reset_password', {"request": request, "token": token, "error": "Passwords do not match.", "success": False})
26
+ # Validate filename format
27
+ if not filename or not filename.replace('-', '').replace('_', '').isalnum():
28
+ print('1')
29
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid file format", "success": False})
30
+ return HTMLResponse(content=html)
31
+
32
+ if not os.path.exists(file_path) or not os.path.isfile(file_path):
33
+ print('2')
34
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Reset file not found or expired", "success": False})
35
+ return HTMLResponse(content=html)
36
+
37
+ # File exists and is actually a file, show the form
38
+ html = await render('reset_password', {"request": request, "token": filename, "error": None, "success": False})
39
+ return HTMLResponse(content=html)
18
40
 
41
+ @public_route()
42
+ @router.post("/reset-password/{filename}")
43
+ async def handle_reset_password_by_file(request: Request, filename: str, password: str = Form(...), confirm_password: str = Form(...), services: ProviderManager = Depends(lambda: service_manager)):
44
+ """Handle password reset using trigger file"""
45
+ trigger_dir = "data/password_resets"
46
+ file_path = os.path.join(trigger_dir, f"{filename}")
47
+
48
+ # Validate filename format
49
+ if not filename or not filename.replace('-', '').replace('_', '').isalnum():
50
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid file format", "success": False})
51
+ return HTMLResponse(content=html)
52
+
53
+ if password != confirm_password:
54
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Passwords do not match.", "success": False})
55
+ return HTMLResponse(content=html)
56
+
57
+ if not os.path.exists(file_path) or not os.path.isfile(file_path):
58
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Reset file not found or expired.", "success": False})
59
+ return HTMLResponse(content=html)
60
+
19
61
  try:
20
- success = await services.get('user_service.reset_password_with_token')(token=token, new_password=password)
62
+ # Read the trigger file
63
+ with open(file_path, 'r') as f:
64
+ data = f.read().strip()
65
+
66
+ # Parse the file content: "username is_admin_reset"
67
+ parts = data.split(' ')
68
+ if len(parts) != 2:
69
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid reset file format.", "success": False})
70
+ return HTMLResponse(content=html)
71
+
72
+ username, is_admin_reset_str = parts
73
+ is_admin_reset = is_admin_reset_str.lower() == 'true'
74
+
75
+ if not username:
76
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid reset file format.", "success": False})
77
+ return HTMLResponse(content=html)
78
+
79
+ logger.info(f"Processing password reset for user: {username} from file: {filename}")
80
+
81
+ # Generate token and reset password
82
+ token = await initiate_password_reset(username=username, is_admin_reset=is_admin_reset)
83
+ success = await reset_password_with_token(token=token, new_password=password)
84
+
21
85
  if success:
22
- return await render('reset_password', {"request": request, "token": token, "error": None, "success": True})
86
+ # Delete the trigger file
87
+ os.remove(file_path)
88
+ logger.info(f"Successfully reset password for {username}, removed trigger file: {filename}")
89
+ html = await render('reset_password', {"request": request, "token": filename, "error": None, "success": True})
90
+ return HTMLResponse(content=html)
23
91
  else:
24
- return await render('reset_password', {"request": request, "token": token, "error": "Invalid or expired token.", "success": False})
25
- except ValueError as e:
26
- return await render('reset_password', {"request": request, "token": token, "error": str(e), "success": False})
27
-
28
- # This is an admin-only function to generate a reset link.
29
- # In a real app, this would be more protected.
30
- @router.get("/admin/initiate-reset/{username}")
31
- async def admin_initiate_reset(username: str, services: ProviderManager = Depends(lambda: service_manager)):
32
- try:
33
- token = await services.get('user_service.initiate_password_reset')(username=username)
34
- return HTMLResponse(f'<h1>Password Reset Link</h1><p>Share this link with the user: <a href="/user_service/reset-password/{token}">/user_service/reset-password/{token}</a></p>')
35
- except ValueError as e:
36
- return HTMLResponse(f'<h1>Error</h1><p>{str(e)}</p>', status_code=404)
92
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Password reset failed.", "success": False})
93
+ return HTMLResponse(content=html)
94
+
95
+ except Exception as e:
96
+ logger.error(f"Error processing trigger file {filename}: {e}")
97
+ html = await render('reset_password', {"request": request, "token": filename, "error": f"Error processing reset: {str(e)}", "success": False})
98
+ return HTMLResponse(content=html)
@@ -0,0 +1,28 @@
1
+ from typing import Optional, Dict
2
+
3
+ async def verify_api_key(api_key: str) -> Optional[Dict]:
4
+ """
5
+ Verify an API key and return user data if valid.
6
+
7
+ Args:
8
+ api_key: The API key to verify
9
+
10
+ Returns:
11
+ Dict containing user data if valid, None otherwise
12
+ """
13
+ try:
14
+ # Import here to avoid circular imports
15
+ from mindroot.coreplugins.api_keys.api_key_manager import api_key_manager
16
+
17
+ key_data = api_key_manager.validate_key(api_key)
18
+ if key_data:
19
+ return {
20
+ 'username': key_data['username'],
21
+ 'api_key': api_key,
22
+ 'created_at': key_data['created_at'],
23
+ 'description': key_data.get('description', '')
24
+ }
25
+ return None
26
+ except Exception as e:
27
+ print(f"Error verifying API key: {e}")
28
+ return None
@@ -0,0 +1,94 @@
1
+ import sys
2
+ import os
3
+ import asyncio
4
+ from termcolor import colored
5
+ from ..plugins.installation import download_github_files
6
+ from ..plugins.manifest import update_plugin_manifest
7
+
8
+ async def _stream_subprocess(cmd):
9
+ process = await asyncio.create_subprocess_exec(
10
+ *cmd,
11
+ stdout=asyncio.subprocess.PIPE,
12
+ stderr=asyncio.subprocess.PIPE
13
+ )
14
+
15
+ async def read_stream(stream, prefix):
16
+ while True:
17
+ line = await stream.readline()
18
+ if line:
19
+ print(f"{prefix}{line.decode().strip()}")
20
+ else:
21
+ break
22
+
23
+ await asyncio.gather(
24
+ read_stream(process.stdout, ''),
25
+ read_stream(process.stderr, colored('ERR: ', 'red'))
26
+ )
27
+
28
+ await process.wait()
29
+ return process.returncode
30
+
31
+ async def install_plugins_from_cli(plugin_sources: list, reinstall: bool = False):
32
+ """
33
+ Install plugins from the command line, streaming output.
34
+ """
35
+ print(colored(f"Attempting to install {len(plugin_sources)} plugins...", "cyan"))
36
+ results = []
37
+
38
+ for plugin_source in plugin_sources:
39
+ plugin_name = plugin_source.split('/')[-1]
40
+ print(colored(f"\n=== Installing {plugin_name} ===", "yellow"))
41
+
42
+ try:
43
+ if not reinstall:
44
+ import pkg_resources
45
+ try:
46
+ pkg_resources.get_distribution(plugin_name)
47
+ print(colored(f"{plugin_name} is already installed. Use --reinstall to update or force.", "green"))
48
+ results.append({"plugin": plugin_name, "status": "already_installed"})
49
+ continue
50
+ except pkg_resources.DistributionNotFound:
51
+ pass # Not installed, proceed
52
+
53
+ if '/' in plugin_source: # GitHub source
54
+ print(f"Installing from GitHub: {plugin_source}")
55
+ plugin_dir, _, plugin_info = download_github_files(plugin_source)
56
+ cmd = [sys.executable, '-m', 'pip', 'install', '-e', plugin_dir]
57
+ if reinstall:
58
+ cmd.append('--force-reinstall')
59
+ return_code = await _stream_subprocess(cmd)
60
+
61
+ if return_code == 0:
62
+ print(colored(f"Successfully installed {plugin_name} from {plugin_source}", "green"))
63
+ update_plugin_manifest(
64
+ plugin_info['name'],
65
+ 'github',
66
+ os.path.abspath(plugin_dir),
67
+ remote_source=plugin_source,
68
+ version=plugin_info.get('version', '0.0.1'),
69
+ metadata=plugin_info
70
+ )
71
+ results.append({"plugin": plugin_name, "status": "success", "source": "github"})
72
+ else:
73
+ raise Exception(f"pip install failed with exit code {return_code}")
74
+
75
+ else: # PyPI source
76
+ print(f"Installing from PyPI: {plugin_name}")
77
+ cmd = [sys.executable, '-m', 'pip', 'install', plugin_name]
78
+ if reinstall:
79
+ cmd.extend(['--upgrade', '--force-reinstall'])
80
+ return_code = await _stream_subprocess(cmd)
81
+
82
+ if return_code == 0:
83
+ print(colored(f"Successfully installed {plugin_name} from PyPI", "green"))
84
+ update_plugin_manifest(plugin_name, 'pypi', None)
85
+ results.append({"plugin": plugin_name, "status": "success", "source": "pypi"})
86
+ else:
87
+ raise Exception(f"pip install failed with exit code {return_code}")
88
+
89
+ except Exception as e:
90
+ print(colored(f"ERROR: Failed to install {plugin_name}: {str(e)}", "red"))
91
+ results.append({"plugin": plugin_name, "status": "error", "message": str(e)})
92
+
93
+ print(colored("\nPlugin installation process finished.", "cyan"))
94
+ # You can optionally print a summary of results here
@@ -21,10 +21,18 @@
21
21
  "enabled": true,
22
22
  "source": "core"
23
23
  },
24
+ "l8n": {
25
+ "enabled": true,
26
+ "source": "core"
27
+ },
24
28
  "jwt_auth": {
25
29
  "enabled": true,
26
30
  "source": "core"
27
31
  },
32
+ "mcp_": {
33
+ "enabled": true,
34
+ "source": "core"
35
+ },
28
36
  "api_keys": {
29
37
  "enabled": true,
30
38
  "source": "core"
@@ -45,6 +53,14 @@
45
53
  "enabled": true,
46
54
  "source": "core"
47
55
  },
56
+ "signup": {
57
+ "enabled": true,
58
+ "source": "core"
59
+ },
60
+ "email": {
61
+ "enabled": true,
62
+ "source": "core"
63
+ },
48
64
  "user_service": {
49
65
  "enabled": true,
50
66
  "source": "core"
@@ -65,6 +81,10 @@
65
81
  "enabled": false,
66
82
  "source": "core"
67
83
  },
84
+ "subscriptions": {
85
+ "enabled": false,
86
+ "source": "core"
87
+ },
68
88
  "startup": {
69
89
  "enabled": true,
70
90
  "source": "core"
@@ -282,12 +282,12 @@ async def install_recommended_plugins(agent_name, context=None):
282
282
  for plugin_source in recommended_plugins:
283
283
  for index in available_indices:
284
284
  for plugin in index.get('plugins', []):
285
- remote_source = plugin.get('remote_source', plugin.get('github_url', plugin.get('source')))
286
- print(f"Checking plugin Index plugin {remote_source} against recommended {plugin_source}")
285
+ remote_source = plugin.get('remote_source') or plugin.get('github_url') or plugin.get('source')
286
+ print(f"Checking index plugin {remote_source} against recommended {plugin_source}")
287
287
  if remote_source == plugin_source:
288
- if 'github.com/' in remote_source:
289
- github_path = github_url.split('github.com/')[1]
290
- print(f"Extracted GitHub path for {plugin_name}: {github_path}")
288
+ if remote_source and 'github.com/' in remote_source:
289
+ github_path = remote_source.split('github.com/')[1]
290
+ print(f"Extracted GitHub path for {plugin_source}: {github_path}")
291
291
  plugin_sources[plugin_source] = github_path
292
292
  elif remote_source:
293
293
  plugin_sources[plugin_source] = remote_source