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.
- mindroot/coreplugins/admin/__init__.py +3 -1
- mindroot/coreplugins/admin/agent_router.py +250 -7
- mindroot/coreplugins/admin/asset_manager.py +164 -0
- mindroot/coreplugins/admin/command_router.py +236 -1
- mindroot/coreplugins/admin/mcp_catalog_routes.py +156 -0
- mindroot/coreplugins/admin/mcp_publish_routes.py +450 -0
- mindroot/coreplugins/admin/mcp_registry_routes.py +495 -0
- mindroot/coreplugins/admin/mcp_routes.py +216 -0
- mindroot/coreplugins/admin/mod.py +62 -0
- mindroot/coreplugins/admin/oauth_callback_router.py +84 -0
- mindroot/coreplugins/admin/persona_handler.py +15 -6
- mindroot/coreplugins/admin/persona_router.py +158 -2
- mindroot/coreplugins/admin/plugin_manager.py +63 -0
- mindroot/coreplugins/admin/plugin_router.py +1 -1
- mindroot/coreplugins/admin/plugin_router_fixed.py +23 -0
- mindroot/coreplugins/admin/plugin_router_new_not_working.py +145 -0
- mindroot/coreplugins/admin/plugin_routes.py +114 -0
- mindroot/coreplugins/admin/registry_settings_routes.py +140 -0
- mindroot/coreplugins/admin/router.py +116 -15
- mindroot/coreplugins/admin/service_models.py +1 -1
- mindroot/coreplugins/admin/settings_router.py +1 -0
- mindroot/coreplugins/admin/static/css/admin-custom.css +357 -2
- mindroot/coreplugins/admin/static/css/dark.css +1 -0
- mindroot/coreplugins/admin/static/css/default.css +4 -0
- mindroot/coreplugins/admin/static/js/about-info.js +367 -0
- mindroot/coreplugins/admin/static/js/agent-form.js +83 -3
- mindroot/coreplugins/admin/static/js/api-key-script.js +307 -0
- mindroot/coreplugins/admin/static/js/mcp-manager.js +348 -0
- mindroot/coreplugins/admin/static/js/mcp-publisher.js +780 -0
- mindroot/coreplugins/admin/static/js/persona-editor.js +34 -5
- mindroot/coreplugins/admin/static/js/plugin-toggle.js +1 -1
- mindroot/coreplugins/admin/static/js/recommended-plugin-install.js +63 -0
- mindroot/coreplugins/admin/static/js/registry-auth-section.js +132 -0
- mindroot/coreplugins/admin/static/js/registry-manager-base.js +613 -0
- mindroot/coreplugins/admin/static/js/registry-manager-old.js +385 -0
- mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
- mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
- mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
- mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
- mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
- mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
- mindroot/coreplugins/admin/static/js/registry-shared-services.js +857 -0
- mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
- mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
- mindroot/coreplugins/admin/static/logo.png +0 -0
- mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
- mindroot/coreplugins/agent/Assistant/agent.json +27 -11
- mindroot/coreplugins/agent/agent.py +2 -2
- mindroot/coreplugins/agent/command_parser.py +25 -10
- mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
- mindroot/coreplugins/chat/__init__.py +4 -1
- mindroot/coreplugins/chat/router.py +132 -20
- mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
- mindroot/coreplugins/chat/services.py +31 -1
- mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
- mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
- mindroot/coreplugins/chat/static/css/dark.css +24 -3
- mindroot/coreplugins/chat/static/css/default.css +24 -3
- mindroot/coreplugins/chat/static/css/main.css +1 -0
- mindroot/coreplugins/chat/static/js/action.js +137 -60
- mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
- mindroot/coreplugins/chat/static/js/chat.js +59 -16
- mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
- mindroot/coreplugins/chat/static/js/chatform.js +2 -2
- mindroot/coreplugins/chat/static/site.webmanifest +1 -1
- mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
- mindroot/coreplugins/chat/widget_manager.py +139 -0
- mindroot/coreplugins/chat/widget_routes.py +287 -0
- mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
- mindroot/coreplugins/email/__init__.py +2 -0
- mindroot/coreplugins/email/email_provider.py +2 -2
- mindroot/coreplugins/email/mod.py +100 -0
- mindroot/coreplugins/email/services.py +5 -3
- mindroot/coreplugins/email/smtp_handler.py +9 -3
- mindroot/coreplugins/email/test_email_service.py +75 -0
- mindroot/coreplugins/env_manager/mod.py +61 -25
- mindroot/coreplugins/home/router.py +37 -2
- mindroot/coreplugins/home/static/imgs/logo.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
- mindroot/coreplugins/home/templates/home.jinja2 +15 -6
- mindroot/coreplugins/index/handlers/plugin_ops.py +1 -1
- mindroot/coreplugins/index/indices/default/index.json +6 -6
- mindroot/coreplugins/jwt_auth/middleware.py +47 -1
- mindroot/coreplugins/jwt_auth/mod.py +40 -17
- mindroot/coreplugins/l8n/__init__.py +6 -0
- mindroot/coreplugins/l8n/debug_loader.py +85 -0
- mindroot/coreplugins/l8n/debug_middleware.py +74 -0
- mindroot/coreplugins/l8n/l8n_constants.py +19 -0
- mindroot/coreplugins/l8n/language_detection.py +183 -0
- mindroot/coreplugins/l8n/middleware.py +151 -0
- mindroot/coreplugins/l8n/mod.py +277 -0
- mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
- mindroot/coreplugins/l8n/test_enhanced.py +298 -0
- mindroot/coreplugins/l8n/test_l8n.py +95 -0
- mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
- mindroot/coreplugins/l8n/test_middleware.py +272 -0
- mindroot/coreplugins/l8n/utils.py +232 -0
- mindroot/coreplugins/mcp_/__init__.py +14 -0
- mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
- mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
- mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
- mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
- mindroot/coreplugins/mcp_/mod.py +367 -0
- mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
- mindroot/coreplugins/mcp_/server_installer.py +79 -0
- mindroot/coreplugins/mcp_/setup.py +26 -0
- mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
- mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
- mindroot/coreplugins/persona/mod.py +12 -7
- mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
- mindroot/coreplugins/subscriptions/__init__.py +1 -0
- mindroot/coreplugins/subscriptions/mod.py +14 -3
- mindroot/coreplugins/subscriptions/router.py +3 -0
- mindroot/coreplugins/user_service/__init__.py +1 -2
- mindroot/coreplugins/user_service/admin_init.py +1 -0
- mindroot/coreplugins/user_service/email_service.py +72 -17
- mindroot/coreplugins/user_service/mod.py +10 -2
- mindroot/coreplugins/user_service/password_reset_service.py +180 -27
- mindroot/coreplugins/user_service/router.py +84 -22
- mindroot/lib/auth/api_key.py +28 -0
- mindroot/lib/cli/plugins.py +94 -0
- mindroot/lib/plugins/default_plugin_manifest.json +20 -0
- mindroot/lib/plugins/installation.py +5 -5
- mindroot/lib/plugins/l8n_static_handler.py +225 -0
- mindroot/lib/plugins/loader.py +33 -3
- mindroot/lib/plugins/loader_with_l8n.py +281 -0
- mindroot/lib/plugins/manifest.py +238 -17
- mindroot/lib/providers/commands.py +3 -1
- mindroot/lib/route_decorators.py +5 -5
- mindroot/lib/templates.py +183 -11
- mindroot/lib/utils/merge_arrays.py +1 -1
- mindroot/migrate.py +49 -0
- mindroot/registry/data_access.py +1 -1
- mindroot/server.py +47 -13
- mindroot/server_missing_normal_args.py +197 -0
- mindroot/server_prev.py +173 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/METADATA +7 -2
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/RECORD +147 -114
- mindroot/coreplugins/admin/static/favicon/about.txt +0 -6
- mindroot/coreplugins/admin/static/favicon/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/admin/static/favicon/apple-touch-icon.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon-16x16.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon-32x32.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon.ico +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/about.txt +0 -6
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon.ico +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/site.webmanifest +0 -1
- mindroot/coreplugins/admin/static/favicon/logo.png +0 -0
- mindroot/coreplugins/admin/static/favicon/site.webmanifest +0 -1
- mindroot/coreplugins/admin/static/js/backup/agent-editor.js +0 -186
- mindroot/coreplugins/admin/static/js/backup/agent-form.js +0 -1133
- mindroot/coreplugins/admin/static/js/backup/agent-list.js +0 -94
- mindroot/coreplugins/chat/static/favicon/about.txt +0 -6
- mindroot/coreplugins/chat/static/favicon/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/chat/static/favicon/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/chat/static/favicon/apple-touch-icon.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon-16x16.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon-32x32.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon.ico +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/about.txt +0 -6
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon.ico +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/site.webmanifest +0 -1
- mindroot/coreplugins/chat/static/favicon/logo.png +0 -0
- mindroot/coreplugins/chat/static/favicon/site.webmanifest +0 -1
- mindroot/coreplugins/index/default.json +0 -76
- mindroot/coreplugins/user_service/file_trigger_service.py +0 -72
- mindroot/coreplugins/user_service/hooks.py +0 -23
- /mindroot/coreplugins/{admin/static/favicon/android-chrome-192x192.png → home/static/imgs/backuplogo.png} +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/WHEEL +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/entry_points.txt +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
auth_data.
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
@
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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'
|
|
286
|
-
print(f"Checking
|
|
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 =
|
|
290
|
-
print(f"Extracted GitHub path for {
|
|
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
|