mindroot 9.2.0__py3-none-any.whl → 9.3.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.
Potentially problematic release.
This version of mindroot might be problematic. Click here for more details.
- mindroot/coreplugins/admin/plugin_router.py +1 -1
- mindroot/coreplugins/index/handlers/plugin_ops.py +1 -1
- mindroot/coreplugins/jwt_auth/middleware.py +2 -1
- mindroot/coreplugins/user_service/file_trigger_service.py +3 -63
- mindroot/coreplugins/user_service/password_reset_service.py +180 -27
- mindroot/coreplugins/user_service/router.py +82 -22
- mindroot/lib/plugins/manifest.py +16 -7
- mindroot/migrate.py +30 -0
- mindroot/server.py +5 -0
- {mindroot-9.2.0.dist-info → mindroot-9.3.0.dist-info}/METADATA +1 -1
- {mindroot-9.2.0.dist-info → mindroot-9.3.0.dist-info}/RECORD +15 -14
- {mindroot-9.2.0.dist-info → mindroot-9.3.0.dist-info}/WHEEL +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.3.0.dist-info}/entry_points.txt +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.3.0.dist-info}/licenses/LICENSE +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.3.0.dist-info}/top_level.txt +0 -0
|
@@ -21,7 +21,7 @@ def update_plugins(request: PluginUpdateRequest):
|
|
|
21
21
|
with open('plugins.json', 'w') as file:
|
|
22
22
|
json.dump(plugins_data, file, indent=2)
|
|
23
23
|
|
|
24
|
-
plugins.load('plugin_manifest.json')
|
|
24
|
+
plugins.load('data/plugin_manifest.json')
|
|
25
25
|
|
|
26
26
|
return {"message": "Plugins updated successfully"}
|
|
27
27
|
except Exception as e:
|
|
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
|
|
|
12
12
|
async def get_installed_plugin_metadata(plugin_name: str) -> dict:
|
|
13
13
|
"""Get metadata from main plugin manifest"""
|
|
14
14
|
try:
|
|
15
|
-
manifest_path = Path('plugin_manifest.json')
|
|
15
|
+
manifest_path = Path('data/plugin_manifest.json')
|
|
16
16
|
logger.debug(f"Reading main plugin manifest from: {manifest_path}")
|
|
17
17
|
|
|
18
18
|
if not manifest_path.exists():
|
|
@@ -142,7 +142,8 @@ async def middleware(request: Request, call_next):
|
|
|
142
142
|
print("Error checking for static file", e)
|
|
143
143
|
pass
|
|
144
144
|
print("Did not find static file")
|
|
145
|
-
|
|
145
|
+
# Accept explicitly-registered public routes, _or_ the password-reset link which carries its own token
|
|
146
|
+
if request.url.path in public_routes or request.url.path.startswith('/reset-password'):
|
|
146
147
|
print('Public route: ', request.url.path)
|
|
147
148
|
return await call_next(request)
|
|
148
149
|
elif any([request.url.path.startswith(path) for path in public_static]):
|
|
@@ -7,66 +7,6 @@ from lib.providers.services import service, ServiceProvider
|
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
CHECK_INTERVAL_SECONDS = 30
|
|
14
|
-
|
|
15
|
-
@service()
|
|
16
|
-
async def process_password_reset_requests(context=None):
|
|
17
|
-
"""Processes password reset request files from a directory."""
|
|
18
|
-
# Ensure directories exist
|
|
19
|
-
os.makedirs(REQUEST_DIR, exist_ok=True)
|
|
20
|
-
os.makedirs(GENERATED_DIR, exist_ok=True)
|
|
21
|
-
|
|
22
|
-
sp = ServiceProvider()
|
|
23
|
-
initiate_reset = sp.get('user_service.initiate_password_reset')
|
|
24
|
-
|
|
25
|
-
if not (initiate_reset and callable(initiate_reset)):
|
|
26
|
-
logger.error("Could not get 'user_service.initiate_password_reset' service.")
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
for filename in os.listdir(REQUEST_DIR):
|
|
30
|
-
request_file_path = os.path.join(REQUEST_DIR, filename)
|
|
31
|
-
if not os.path.isfile(request_file_path):
|
|
32
|
-
continue
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
with open(request_file_path, 'r') as f:
|
|
36
|
-
data = json.load(f)
|
|
37
|
-
|
|
38
|
-
username = data.get("username")
|
|
39
|
-
is_admin_reset = data.get("is_admin_reset", False)
|
|
40
|
-
|
|
41
|
-
if not username:
|
|
42
|
-
raise ValueError("Username is missing from request file.")
|
|
43
|
-
|
|
44
|
-
logger.info(f"Processing password reset request for user: {username}")
|
|
45
|
-
token = await initiate_reset(username=username, is_admin_reset=is_admin_reset)
|
|
46
|
-
|
|
47
|
-
reset_link = f"/user_service/reset-password/{token}"
|
|
48
|
-
generated_file_path = os.path.join(GENERATED_DIR, f"{username}_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}.json")
|
|
49
|
-
with open(generated_file_path, 'w') as f:
|
|
50
|
-
json.dump({"username": username, "reset_link": reset_link, "token": token}, f, indent=2)
|
|
51
|
-
|
|
52
|
-
logger.info(f"Successfully generated password reset link for {username}.")
|
|
53
|
-
os.remove(request_file_path)
|
|
54
|
-
|
|
55
|
-
except Exception as e:
|
|
56
|
-
logger.error(f"Failed to process reset request file {filename}: {e}")
|
|
57
|
-
error_file_path = os.path.join(REQUEST_DIR, f"{filename}.error")
|
|
58
|
-
os.rename(request_file_path, error_file_path)
|
|
59
|
-
|
|
60
|
-
@service()
|
|
61
|
-
async def start_file_watcher_service(context=None):
|
|
62
|
-
"""Starts a background task to watch for password reset request files."""
|
|
63
|
-
logger.info("Starting password reset file watcher service.")
|
|
64
|
-
async def watcher_loop():
|
|
65
|
-
while True:
|
|
66
|
-
try:
|
|
67
|
-
await process_password_reset_requests()
|
|
68
|
-
except Exception as e:
|
|
69
|
-
logger.error(f"Error in password reset watcher loop: {e}")
|
|
70
|
-
await asyncio.sleep(CHECK_INTERVAL_SECONDS)
|
|
71
|
-
|
|
72
|
-
asyncio.create_task(watcher_loop())
|
|
10
|
+
# This file is kept for backward compatibility
|
|
11
|
+
# Password reset trigger functionality is now handled directly in router.py
|
|
12
|
+
logger.info("File trigger service loaded - functionality moved to router.py")
|
|
@@ -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,96 @@
|
|
|
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
|
+
trigger_dir = "data/password_resets"
|
|
21
|
+
file_path = os.path.join(trigger_dir, f"{filename}")
|
|
22
|
+
print("file path", file_path)
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
# Validate filename format
|
|
25
|
+
if not filename or not filename.replace('-', '').replace('_', '').isalnum():
|
|
26
|
+
print('1')
|
|
27
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid file format", "success": False})
|
|
28
|
+
return HTMLResponse(content=html)
|
|
29
|
+
|
|
30
|
+
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
|
31
|
+
print('2')
|
|
32
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Reset file not found or expired", "success": False})
|
|
33
|
+
return HTMLResponse(content=html)
|
|
34
|
+
|
|
35
|
+
# File exists and is actually a file, show the form
|
|
36
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": None, "success": False})
|
|
37
|
+
return HTMLResponse(content=html)
|
|
18
38
|
|
|
39
|
+
@public_route()
|
|
40
|
+
@router.post("/reset-password/{filename}")
|
|
41
|
+
async def handle_reset_password_by_file(request: Request, filename: str, password: str = Form(...), confirm_password: str = Form(...), services: ProviderManager = Depends(lambda: service_manager)):
|
|
42
|
+
"""Handle password reset using trigger file"""
|
|
43
|
+
trigger_dir = "data/password_resets"
|
|
44
|
+
file_path = os.path.join(trigger_dir, f"{filename}")
|
|
45
|
+
|
|
46
|
+
# Validate filename format
|
|
47
|
+
if not filename or not filename.replace('-', '').replace('_', '').isalnum():
|
|
48
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid file format", "success": False})
|
|
49
|
+
return HTMLResponse(content=html)
|
|
50
|
+
|
|
51
|
+
if password != confirm_password:
|
|
52
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Passwords do not match.", "success": False})
|
|
53
|
+
return HTMLResponse(content=html)
|
|
54
|
+
|
|
55
|
+
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
|
56
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Reset file not found or expired.", "success": False})
|
|
57
|
+
return HTMLResponse(content=html)
|
|
58
|
+
|
|
19
59
|
try:
|
|
20
|
-
|
|
60
|
+
# Read the trigger file
|
|
61
|
+
with open(file_path, 'r') as f:
|
|
62
|
+
data = f.read().strip()
|
|
63
|
+
|
|
64
|
+
# Parse the file content: "username is_admin_reset"
|
|
65
|
+
parts = data.split(' ')
|
|
66
|
+
if len(parts) != 2:
|
|
67
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid reset file format.", "success": False})
|
|
68
|
+
return HTMLResponse(content=html)
|
|
69
|
+
|
|
70
|
+
username, is_admin_reset_str = parts
|
|
71
|
+
is_admin_reset = is_admin_reset_str.lower() == 'true'
|
|
72
|
+
|
|
73
|
+
if not username:
|
|
74
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid reset file format.", "success": False})
|
|
75
|
+
return HTMLResponse(content=html)
|
|
76
|
+
|
|
77
|
+
logger.info(f"Processing password reset for user: {username} from file: {filename}")
|
|
78
|
+
|
|
79
|
+
# Generate token and reset password
|
|
80
|
+
token = await initiate_password_reset(username=username, is_admin_reset=is_admin_reset)
|
|
81
|
+
success = await reset_password_with_token(token=token, new_password=password)
|
|
82
|
+
|
|
21
83
|
if success:
|
|
22
|
-
|
|
84
|
+
# Delete the trigger file
|
|
85
|
+
os.remove(file_path)
|
|
86
|
+
logger.info(f"Successfully reset password for {username}, removed trigger file: {filename}")
|
|
87
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": None, "success": True})
|
|
88
|
+
return HTMLResponse(content=html)
|
|
23
89
|
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)
|
|
90
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": "Password reset failed.", "success": False})
|
|
91
|
+
return HTMLResponse(content=html)
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Error processing trigger file {filename}: {e}")
|
|
95
|
+
html = await render('reset_password', {"request": request, "token": filename, "error": f"Error processing reset: {str(e)}", "success": False})
|
|
96
|
+
return HTMLResponse(content=html)
|
mindroot/lib/plugins/manifest.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import shutil
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
|
|
5
6
|
# Central definition of manifest file location
|
|
6
|
-
MANIFEST_FILE = 'plugin_manifest.json'
|
|
7
|
+
MANIFEST_FILE = 'data/plugin_manifest.json'
|
|
7
8
|
|
|
8
9
|
def load_plugin_manifest():
|
|
9
10
|
"""Load the plugin manifest file.
|
|
@@ -68,12 +69,20 @@ def update_plugin_manifest(plugin_name, source, source_path, remote_source=None,
|
|
|
68
69
|
|
|
69
70
|
def create_default_plugin_manifest():
|
|
70
71
|
"""Create a new default manifest file."""
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
# First check if there's an existing manifest in the root directory
|
|
73
|
+
old_manifest_path = 'plugin_manifest.json'
|
|
74
|
+
if os.path.exists(old_manifest_path):
|
|
75
|
+
# Move existing manifest from root to data directory
|
|
76
|
+
os.makedirs(os.path.dirname(MANIFEST_FILE), exist_ok=True)
|
|
77
|
+
shutil.move(old_manifest_path, MANIFEST_FILE)
|
|
78
|
+
else:
|
|
79
|
+
# No existing manifest, create from default template
|
|
80
|
+
# read from default_plugin_manifest.json in same dir as this file
|
|
81
|
+
default_manifest_path = os.path.join(os.path.dirname(__file__), 'default_plugin_manifest.json')
|
|
82
|
+
with open(default_manifest_path, 'r') as f:
|
|
83
|
+
default_manifest = json.load(f)
|
|
84
|
+
os.makedirs(os.path.dirname(MANIFEST_FILE), exist_ok=True)
|
|
85
|
+
save_plugin_manifest(default_manifest)
|
|
77
86
|
def toggle_plugin_state(plugin_name, enabled):
|
|
78
87
|
"""Toggle a plugin's enabled state.
|
|
79
88
|
|
mindroot/migrate.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def migrate_plugin_manifest():
|
|
6
|
+
"""Migrate plugin_manifest.json from root to data/ directory if needed."""
|
|
7
|
+
root_manifest = 'plugin_manifest.json'
|
|
8
|
+
data_manifest = 'data/plugin_manifest.json'
|
|
9
|
+
|
|
10
|
+
# If the new location already exists, no migration needed
|
|
11
|
+
if os.path.exists(data_manifest):
|
|
12
|
+
print(f"Plugin manifest already exists at {data_manifest}")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
# If old location exists, move it to new location
|
|
16
|
+
if os.path.exists(root_manifest):
|
|
17
|
+
print(f"Migrating plugin manifest from {root_manifest} to {data_manifest}")
|
|
18
|
+
# Ensure data directory exists
|
|
19
|
+
os.makedirs('data', exist_ok=True)
|
|
20
|
+
# Move the file
|
|
21
|
+
shutil.move(root_manifest, data_manifest)
|
|
22
|
+
print(f"Plugin manifest migration complete")
|
|
23
|
+
else:
|
|
24
|
+
print("No existing plugin manifest found to migrate")
|
|
25
|
+
|
|
26
|
+
def run_migrations():
|
|
27
|
+
"""Run all necessary migrations."""
|
|
28
|
+
print("Running MindRoot migrations...")
|
|
29
|
+
migrate_plugin_manifest()
|
|
30
|
+
print("Migrations complete")
|
mindroot/server.py
CHANGED
|
@@ -14,6 +14,7 @@ import socket
|
|
|
14
14
|
from fastapi.middleware.cors import CORSMiddleware
|
|
15
15
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
16
16
|
from dotenv import load_dotenv
|
|
17
|
+
from .migrate import run_migrations
|
|
17
18
|
|
|
18
19
|
# Load environment variables from .env file at the start
|
|
19
20
|
# Set override=True to make .env variables override existing environment variables
|
|
@@ -35,6 +36,7 @@ def get_project_root():
|
|
|
35
36
|
def create_directories():
|
|
36
37
|
root = get_project_root()
|
|
37
38
|
directories = [
|
|
39
|
+
"data",
|
|
38
40
|
"imgs",
|
|
39
41
|
"models",
|
|
40
42
|
"models/face",
|
|
@@ -102,6 +104,9 @@ class HeaderMiddleware(BaseHTTPMiddleware):
|
|
|
102
104
|
|
|
103
105
|
def main():
|
|
104
106
|
global app
|
|
107
|
+
|
|
108
|
+
# Run migrations first, before anything else
|
|
109
|
+
run_migrations()
|
|
105
110
|
|
|
106
111
|
cmd_args = parse_args()
|
|
107
112
|
# save ALL parsed args in app state
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
mindroot/__init__.py,sha256=OrFRGt_fdSYjolLXUzjSX2CIn1cOAm6l47ENNAkwmgQ,83
|
|
2
|
-
mindroot/
|
|
2
|
+
mindroot/migrate.py,sha256=kVmcvtXkXRj0weVvPfwTKvdhv1VVYgPLuFnOY0XXxn4,1071
|
|
3
|
+
mindroot/server.py,sha256=mKqO1BNT9i5N5xTuLXPLXK7YAF-9AXNMeDP4rODBGJI,5015
|
|
3
4
|
mindroot/coreplugins/admin/__init__.py,sha256=388n_hMskU0TnZ4xT10US_kFkya-EPBjWcv7AZf_HOk,74
|
|
4
5
|
mindroot/coreplugins/admin/agent_importer.py,sha256=8hQLO64iKtPA5gv9-mLUfUCcnRp3IH9-sTfgkPsrmqo,6376
|
|
5
6
|
mindroot/coreplugins/admin/agent_router.py,sha256=mstYUURNRb9MpfNwqMYC7WY3FOQJ-5H0TNNz4mDSjFM,9220
|
|
@@ -8,7 +9,7 @@ mindroot/coreplugins/admin/persona_handler.py,sha256=eRYmmOmupLJuiujWN35SS2nwRgo
|
|
|
8
9
|
mindroot/coreplugins/admin/persona_router.py,sha256=tyPxBe-pRnntHo_xyazvnwKZxHmvEI5_Fsu0YkZbrXA,6036
|
|
9
10
|
mindroot/coreplugins/admin/plugin_manager.py,sha256=ZXzxSNY6Y0_PXvnZSsSK3WDTkgDJNDuDlFP8y-F72h8,15355
|
|
10
11
|
mindroot/coreplugins/admin/plugin_manager_backup.py,sha256=pUnh-EW4BMOrL2tYUq1X2BC65aoTBhDM-o2-mlDYXfU,24583
|
|
11
|
-
mindroot/coreplugins/admin/plugin_router.py,sha256=
|
|
12
|
+
mindroot/coreplugins/admin/plugin_router.py,sha256=SAxq2ladUyfENlzVr4rSZ0itMKvBJPl37mxgwjfQQD8,1025
|
|
12
13
|
mindroot/coreplugins/admin/router.py,sha256=Wzeri92Rv39YKTYflMZ3E1pnYy959KHWRjS8D4Xgaao,1580
|
|
13
14
|
mindroot/coreplugins/admin/server_router.py,sha256=aZ5v7QBcK1LJ2yiciUA4elY26iSGl-8m03L1h3ogvP4,4533
|
|
14
15
|
mindroot/coreplugins/admin/service_models.py,sha256=ccYdLjWlEbbkIq9y9T5TL8NGSO6m1LLiJb9J2wkbenY,3354
|
|
@@ -1310,7 +1311,7 @@ mindroot/coreplugins/index/utils.py,sha256=UF30B_XM3FNHjwn_BnxaW9yEjwZl-TjyDBcUD
|
|
|
1310
1311
|
mindroot/coreplugins/index/handlers/__init__.py,sha256=MPoCNJooyIGXwD9lXyIR8RQwUE5MrPj4wFpJVIzPCtM,421
|
|
1311
1312
|
mindroot/coreplugins/index/handlers/agent_ops.py,sha256=6faXdvDwZfZSsev5BLlKxJkDo4BNskVNAstWt3_T6kA,3849
|
|
1312
1313
|
mindroot/coreplugins/index/handlers/index_ops.py,sha256=VvGev9qKMtXK0LJnyO4s0fKeJwFLlXIBI3SPgYIGMD8,3898
|
|
1313
|
-
mindroot/coreplugins/index/handlers/plugin_ops.py,sha256=
|
|
1314
|
+
mindroot/coreplugins/index/handlers/plugin_ops.py,sha256=lYSNAvdbXtaTdJToe5Sn30H7PmkKrB3vP34g4NuUkS0,5604
|
|
1314
1315
|
mindroot/coreplugins/index/handlers/publish.py,sha256=J4SiDSvaXpYV7My66nFjD1jHTVeDsV986iIwUAUlLok,4684
|
|
1315
1316
|
mindroot/coreplugins/index/indices/default/index.json,sha256=2zR869dAVESvpGuRATZXaaQkh7n2oqkjc3DY2VcqXgc,37991
|
|
1316
1317
|
mindroot/coreplugins/index/indices/default/personas/Assistant/avatar.png,sha256=AsT2_jjGpZvEhzTEwSVhEShSYaIBhcUDrlj_bAG_HVY,1169266
|
|
@@ -1694,7 +1695,7 @@ mindroot/coreplugins/index/static/js/lit-html/node/directives/until.js.map,sha25
|
|
|
1694
1695
|
mindroot/coreplugins/index/static/js/lit-html/node/directives/when.js,sha256=NLe0NJ-6jqjVDUrT_DzmSpREsRaLo1yarzdYcV_5xHY,181
|
|
1695
1696
|
mindroot/coreplugins/index/static/js/lit-html/node/directives/when.js.map,sha256=tOonih_-EaqrunhNGshA9xN--WIVdGikjg8MkVp0itQ,1534
|
|
1696
1697
|
mindroot/coreplugins/jwt_auth/__init__.py,sha256=qFCBnx0oAKTtMSXiPEa7VXOIlWDTU-5CY0XvodgSUlM,79
|
|
1697
|
-
mindroot/coreplugins/jwt_auth/middleware.py,sha256=
|
|
1698
|
+
mindroot/coreplugins/jwt_auth/middleware.py,sha256=TPz1MZch6BJXtIMlmdQqUO3-xbUy9TMqFDTU8uetfzQ,9388
|
|
1698
1699
|
mindroot/coreplugins/jwt_auth/mod.py,sha256=AfRDh9vyGGTE0qLdEOXl2TZYufYxqjsE34uIDQXq--o,1039
|
|
1699
1700
|
mindroot/coreplugins/jwt_auth/role_checks.py,sha256=bruZIIBSOvXNWB1YZ2s5btrbbXNf18w6MdORpJByV60,1555
|
|
1700
1701
|
mindroot/coreplugins/jwt_auth/router.py,sha256=ecXYao_UG33UjQF15Hi-tf_X0eFsqLEldyqGpt7JNSw,1162
|
|
@@ -1762,13 +1763,13 @@ mindroot/coreplugins/usage/templates/usage.jinja2,sha256=bSAjGbgx-hgzkPR0u1OIoJi
|
|
|
1762
1763
|
mindroot/coreplugins/user_service/__init__.py,sha256=YdQYM2GS6I2vB6r4ilODQjcV6LKTnKlCmo1EbfCQeVE,171
|
|
1763
1764
|
mindroot/coreplugins/user_service/admin_init.py,sha256=wQ28wmohiER0iMEyz4kypSbSDkD38LE7WXSgZyo0-pA,3923
|
|
1764
1765
|
mindroot/coreplugins/user_service/email_service.py,sha256=ptcnvPIxF7kY9NgdDlNZYGirzi6ruY8sWmuMp-fHLgA,1591
|
|
1765
|
-
mindroot/coreplugins/user_service/file_trigger_service.py,sha256=
|
|
1766
|
+
mindroot/coreplugins/user_service/file_trigger_service.py,sha256=ryHY_YBlQYLGyOO59PKqC5ijy0DOcaXLtvIQOaumELE,381
|
|
1766
1767
|
mindroot/coreplugins/user_service/hooks.py,sha256=4di2j0tVhEKplDaG9fTlRtErIflZPYBfC3iHGBtUgh0,875
|
|
1767
1768
|
mindroot/coreplugins/user_service/mod.py,sha256=CW0CtNp4EOg3HsNS-PlwVVy4F5q0v0RJyAvifKVhFRM,4856
|
|
1768
1769
|
mindroot/coreplugins/user_service/models.py,sha256=s_dy881Ob8Ng94PQ-Iw9lYq306l3QsnDLVk7e3OED5w,1137
|
|
1769
|
-
mindroot/coreplugins/user_service/password_reset_service.py,sha256=
|
|
1770
|
+
mindroot/coreplugins/user_service/password_reset_service.py,sha256=KsZ4VxCplJHWI0wNc-8dUiWNV6l74FIw3HiWCSdP8aI,10537
|
|
1770
1771
|
mindroot/coreplugins/user_service/role_service.py,sha256=e6XrxhMC4903C-Y515XSC544uXAik6-CSee-TIPGIwA,2329
|
|
1771
|
-
mindroot/coreplugins/user_service/router.py,sha256=
|
|
1772
|
+
mindroot/coreplugins/user_service/router.py,sha256=7TxaEYswj4NmQanfW8T15HTu3FoYMR1m3OUJeAPn_Ag,4873
|
|
1772
1773
|
mindroot/coreplugins/user_service/backup/admin_service.py,sha256=scc59rxlZz4uuVvgjf-9HL2gKi7-uiCdSt6LjWJILR8,4259
|
|
1773
1774
|
mindroot/coreplugins/user_service/backup/admin_setup.py,sha256=JGszAw8nVtnNiisSUGu9jtoStKGyN44KpbRlKAhDJho,3001
|
|
1774
1775
|
mindroot/coreplugins/user_service/templates/reset_password.jinja2,sha256=GE2rHNmSUlAS5EoJuu9g3KsUel5RNMKMVYTfxhi2IPM,2097
|
|
@@ -1804,7 +1805,7 @@ mindroot/lib/plugins/__init__.py,sha256=ZpIgOKsGjyvAe9KenqZ_5ppxaQ7br9EmU7RBI_VX
|
|
|
1804
1805
|
mindroot/lib/plugins/default_plugin_manifest.json,sha256=lg3mIQmQPU6nkOKjClstvXGC82tNP9RuV-LLQVIJAIw,1406
|
|
1805
1806
|
mindroot/lib/plugins/installation.py,sha256=Ju3gD3cv9I5qIY0hrL7-4t5xYeivEtEgUveWIiE1oqM,15360
|
|
1806
1807
|
mindroot/lib/plugins/loader.py,sha256=OhBzfzHuZCwxVtloVrgkHUJCaLG3_n4k9lDLMIuuZsg,9541
|
|
1807
|
-
mindroot/lib/plugins/manifest.py,sha256=
|
|
1808
|
+
mindroot/lib/plugins/manifest.py,sha256=33jnEWI-2eNmWRXHVxdQWPovLSiyeD_gTIsna9a8u-M,4474
|
|
1808
1809
|
mindroot/lib/plugins/mapping.py,sha256=B3nxbStLzyYj8CEuEfxWXMbCu_dOtK_zVTbcCBw5mMc,838
|
|
1809
1810
|
mindroot/lib/plugins/paths.py,sha256=vPDjEF9NU1xHMIcnN4_Hf1gjyleyXAAfRcf86WXt4M4,4312
|
|
1810
1811
|
mindroot/lib/providers/__init__.py,sha256=e0DzrmFHxZJCzWf-GE41_n6zjzN1XmL_d-8cBcmYh5U,11681
|
|
@@ -1826,9 +1827,9 @@ mindroot/protocols/services/stream_chat.py,sha256=fMnPfwaB5fdNMBLTEg8BXKAGvrELKH
|
|
|
1826
1827
|
mindroot/registry/__init__.py,sha256=40Xy9bmPHsgdIrOzbtBGzf4XMqXVi9P8oZTJhn0r654,151
|
|
1827
1828
|
mindroot/registry/component_manager.py,sha256=WZFNPg4SNvpqsM5NFiC2DpgmrJQCyR9cNhrCBpp30Qk,995
|
|
1828
1829
|
mindroot/registry/data_access.py,sha256=NgNMamxIjaKeYxzxnVaQz1Y-Rm0AI51si3788_JHUTM,5316
|
|
1829
|
-
mindroot-9.
|
|
1830
|
-
mindroot-9.
|
|
1831
|
-
mindroot-9.
|
|
1832
|
-
mindroot-9.
|
|
1833
|
-
mindroot-9.
|
|
1834
|
-
mindroot-9.
|
|
1830
|
+
mindroot-9.3.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
|
|
1831
|
+
mindroot-9.3.0.dist-info/METADATA,sha256=XifktWZGe9gF5apBJpZIgxLKNikiSXIOPqrGsMmTcus,891
|
|
1832
|
+
mindroot-9.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
1833
|
+
mindroot-9.3.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
|
|
1834
|
+
mindroot-9.3.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
|
|
1835
|
+
mindroot-9.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|