syntaxmatrix 2.5.5.5__py3-none-any.whl → 2.5.6__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.
syntaxmatrix/__init__.py CHANGED
@@ -44,8 +44,8 @@ get_widget_value = _app_instance.get_widget_value
44
44
  save_embed_model = _app_instance.save_embed_model
45
45
  load_embed_model = _app_instance.load_embed_model
46
46
  delete_embed_key = _app_instance.delete_embed_key
47
- set_prompt_profile = _app_instance.set_prompt_profile
48
- set_prompt_instructions = _app_instance.set_prompt_instructions
47
+ set_smxai_identity = _app_instance.set_smxai_identity
48
+ set_smxai_instructions = _app_instance.set_smxai_instructions
49
49
  set_website_description = _app_instance.set_website_description
50
50
  smiv_index = _app_instance.smiv_index
51
51
  smpv_search = _app_instance.smpv_search
@@ -54,6 +54,7 @@ process_query_stream = _app_instance.process_query_stream
54
54
  process_query = _app_instance.process_query
55
55
  embed_query = _app_instance.embed_query
56
56
  enable_user_files = _app_instance.enable_user_files
57
+ enable_registration = _app_instance.enable_registration
57
58
  stream_write = _app_instance.stream_write
58
59
  enable_stream = _app_instance.enable_stream
59
60
  stream = _app_instance.stream
syntaxmatrix/auth.py CHANGED
@@ -41,6 +41,19 @@ def init_auth_db():
41
41
  );
42
42
  """)
43
43
 
44
+ # Ensure new must_reset_password flag exists for mandatory first-login reset
45
+ try:
46
+ cur = conn.execute("PRAGMA table_info(users)")
47
+ cols = [row[1] for row in cur.fetchall()]
48
+ if "must_reset_password" not in cols:
49
+ conn.execute(
50
+ "ALTER TABLE users "
51
+ "ADD COLUMN must_reset_password INTEGER NOT NULL DEFAULT 0"
52
+ )
53
+ except Exception:
54
+ # Best-effort migration; if this fails we still let the app start
55
+ pass
56
+
44
57
  # --- Roles table ---
45
58
  conn.execute("""
46
59
  CREATE TABLE IF NOT EXISTS roles (
@@ -302,6 +315,60 @@ def register_user(email:str, username:str, password:str, role:str = "user") -> b
302
315
  finally:
303
316
  conn.close()
304
317
 
318
+ def set_must_reset_by_email(email: str, must_reset: bool = True) -> None:
319
+ """
320
+ Mark a user account as requiring a password reset (or clear the flag) by email.
321
+ Used when an admin creates a user with a temporary password.
322
+ """
323
+ if not email:
324
+ return
325
+ conn = _get_conn()
326
+ try:
327
+ conn.execute(
328
+ "UPDATE users SET must_reset_password = ? WHERE email = ?",
329
+ (1 if must_reset else 0, email),
330
+ )
331
+ conn.commit()
332
+ finally:
333
+ conn.close()
334
+
335
+
336
+ def user_must_reset_password(user_id: int) -> bool:
337
+ """
338
+ Check whether this user is currently forced to change their password.
339
+ """
340
+ if not user_id:
341
+ return False
342
+ conn = _get_conn()
343
+ try:
344
+ cur = conn.execute(
345
+ "SELECT must_reset_password FROM users WHERE id = ?",
346
+ (user_id,),
347
+ )
348
+ row = cur.fetchone()
349
+ finally:
350
+ conn.close()
351
+ if not row:
352
+ return False
353
+ return bool(row[0])
354
+
355
+
356
+ def clear_must_reset(user_id: int) -> None:
357
+ """
358
+ Clear the mandatory-reset flag (called after the user has changed their password).
359
+ """
360
+ if not user_id:
361
+ return
362
+ conn = _get_conn()
363
+ try:
364
+ conn.execute(
365
+ "UPDATE users SET must_reset_password = 0 WHERE id = ?",
366
+ (user_id,),
367
+ )
368
+ conn.commit()
369
+ finally:
370
+ conn.close()
371
+
305
372
  def authenticate(email:str, password:str) -> Optional[Dict]:
306
373
  """Return user dict if creds match, else None."""
307
374
  conn = _get_conn()
@@ -315,15 +382,85 @@ def authenticate(email:str, password:str) -> Optional[Dict]:
315
382
  return {"id": row[0], "email":row[1], "username": row[2], "role": row[4]}
316
383
  return None
317
384
 
385
+ def verify_password(user_id: int, candidate: str) -> bool:
386
+ """
387
+ Check whether `candidate` matches the current password of the user.
388
+ Used by the change-password flow.
389
+ """
390
+ if not user_id or not candidate:
391
+ return False
392
+
393
+ conn = _get_conn()
394
+ try:
395
+ cur = conn.execute(
396
+ "SELECT password FROM users WHERE id = ?",
397
+ (user_id,),
398
+ )
399
+ row = cur.fetchone()
400
+ finally:
401
+ conn.close()
402
+
403
+ if not row:
404
+ return False
405
+ return check_password_hash(row[0], candidate)
406
+
407
+
408
+ def update_password(user_id: int, new_password: str) -> None:
409
+ """
410
+ Overwrite the user's password with a new hash.
411
+ """
412
+ if not user_id or not new_password:
413
+ return
414
+
415
+ hashed = generate_password_hash(new_password)
416
+ conn = _get_conn()
417
+ try:
418
+ conn.execute(
419
+ "UPDATE users SET password = ? WHERE id = ?",
420
+ (hashed, user_id),
421
+ )
422
+ conn.commit()
423
+ finally:
424
+ conn.close()
425
+
426
+ def update_password(user_id: int, new_password: str) -> bool:
427
+ """
428
+ Update the stored password hash for a given user id.
429
+ Returns True on success, False if something goes wrong.
430
+ """
431
+ hashed = generate_password_hash(new_password)
432
+ conn = _get_conn()
433
+ try:
434
+ conn.execute(
435
+ "UPDATE users SET password = ? WHERE id = ?",
436
+ (hashed, user_id),
437
+ )
438
+ conn.commit()
439
+ return True
440
+ except Exception:
441
+ # We do not raise inside auth; caller shows a friendly message instead.
442
+ return False
443
+ finally:
444
+ conn.close()
445
+
318
446
  def login_required(f):
319
447
  @wraps(f)
320
- def decorated(*args, **kwargs):
321
- if not session.get("user_id"):
322
- flash("Please log in to access this page.")
448
+ def wrapper(*args, **kwargs):
449
+ if "user_id" not in session:
450
+ flash("Please log in.")
323
451
  return redirect(url_for("login", next=request.path))
324
- return f(*args, **kwargs)
325
- return decorated
326
452
 
453
+ # If the account is flagged for a mandatory reset, force the user
454
+ # to the change-password screen before allowing anything else.
455
+ if session.get("must_reset_password") and request.endpoint not in (
456
+ "change_password",
457
+ "logout",
458
+ ):
459
+ flash("Please set a new password before continuing.")
460
+ return redirect(url_for("change_password"))
461
+
462
+ return f(*args, **kwargs)
463
+ return wrapper
327
464
 
328
465
  def admin_required(view):
329
466
  @wraps(view)
syntaxmatrix/core.py CHANGED
@@ -54,7 +54,7 @@ class SyntaxMUI:
54
54
  port="5080",
55
55
  user_icon="👩🏿‍🦲",
56
56
  bot_icon="<img src='/static/icons/favicon.png' width=20' alt='bot'/>",
57
- favicon="/static/icons/favicon.png",
57
+ favicon="", # /static/icons/favicon.png",
58
58
  site_logo="<img src='/static/icons/logo.png' width='30' alt='logo'/>",
59
59
  site_title="SyntaxMatrix",
60
60
  project_name="smxAI",
@@ -75,6 +75,7 @@ class SyntaxMUI:
75
75
  self.ui_mode = ui_mode
76
76
  self.theme_toggle_enabled = False
77
77
  self.user_files_enabled = False
78
+ self.registration_enabled = False
78
79
  self.smxai_identity = SMXAI_CHAT_ID
79
80
  self.smxai_instructions = SMXAI_CHAT_INSTRUCTIONS
80
81
  self.website_description = SMXAI_WEBSITE_DESCRIPTION
@@ -311,6 +312,9 @@ class SyntaxMUI:
311
312
 
312
313
  def enable_user_files(self):
313
314
  self.user_files_enabled = True
315
+
316
+ def enable_registration(self):
317
+ self.registration_enabled = True
314
318
 
315
319
  @staticmethod
316
320
  def columns(components):
@@ -514,12 +518,12 @@ class SyntaxMUI:
514
518
  # ──────────────────────────────────────────────────────────────
515
519
  # *********** LLM CLIENT HELPERS **********************
516
520
  # ──────────────────────────────────────────────────────────────
517
- def set_prompt_profile(self, profile):
518
- self.ai_chat_id = profile
521
+ def set_smxai_identity(self, profile):
522
+ self.set_smxai_identity = profile
519
523
 
520
524
 
521
- def set_prompt_instructions(self, instructions):
522
- self.ai_chat_instructions = instructions
525
+ def set_smxai_instructions(self, instructions):
526
+ self.set_smxai_instructions = instructions
523
527
 
524
528
 
525
529
  def set_website_description(self, desc):
@@ -35,15 +35,25 @@ _SMX_FADEIN_CSS = '''
35
35
  </style>
36
36
  '''.strip()
37
37
 
38
- _SMX_LAYOUT_CSS = '''
38
+ _SMX_LAYOUT_CSS = """
39
39
  <style>
40
- .smx-hero{max-width:1200px;margin:0 auto;padding:clamp(1.5rem,3vw,2.5rem);}
41
- .smx-hero .smx-img{aspect-ratio:16/9;width:100%;max-height:min(56vh,560px);object-fit:cover;border-radius:1rem;}
42
- .smx-card{border-radius:1rem;box-shadow:0 6px 20px rgba(2,6,23,0.06);}
43
- #smx-root,[id^="smx-"]{margin-top:clamp(16px,2vw,32px);margin-bottom:clamp(24px,3vw,48px);padding-inline:clamp(12px,2vw,24px);}
44
- #smx-root .smx-container,[id^="smx-"] .smx-container{max-width:1200px;margin-inline:auto;}
40
+ #smx-root,
41
+ [id^="smx-"]{
42
+ margin-top: 0;
43
+ margin-bottom: 40px;
44
+ /* No side padding on desktop */
45
+ padding-inline: 0;
46
+ }
47
+
48
+ /* Keep a bit of breathing room on small screens */
49
+ @media (max-width: 768px) {
50
+ #smx-root,
51
+ [id^="smx-"]{
52
+ padding-inline: 12px;
53
+ }
54
+ }
45
55
  </style>
46
- '''.strip()
56
+ """.strip()
47
57
 
48
58
  def smx_strip_fences(html: str) -> str:
49
59
  s = (html or '').strip()