- InlineField component + useInlineEdit composable for Odoo-style dblclick editing - Client search by name, account ID, and legacy_customer_id (or_filters) - SMS/Email notification panel on ContactCard via n8n webhooks - Ticket reply thread via Communication docs - All migration scripts (51 files) now tracked - Client portal and field tech app added to monorepo - README rewritten with full feature list, migration summary, architecture - CHANGELOG updated with all recent work - ROADMAP updated with current completion status - Removed hardcoded tokens from docs (use $ERP_SERVICE_TOKEN) - .gitignore updated (docker/, .claude/, exports/, .quasar/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""
|
|
Install the portal auth bridge as a Server Script in ERPNext.
|
|
This creates a whitelisted API endpoint: /api/method/portal_login
|
|
|
|
Flow:
|
|
1. User POSTs email + password
|
|
2. If user has legacy_password_md5:
|
|
→ md5(password) matches? → update to pbkdf2, clear legacy hash, create session
|
|
→ no match? → error
|
|
3. If no legacy hash: standard frappe auth
|
|
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/setup_portal_auth_bridge.py
|
|
"""
|
|
import os, sys
|
|
os.chdir("/home/frappe/frappe-bench/sites")
|
|
import frappe
|
|
frappe.init(site="erp.gigafibre.ca", sites_path=".")
|
|
frappe.connect()
|
|
print("Connected:", frappe.local.site)
|
|
|
|
SCRIPT_NAME = "Portal Login Bridge"
|
|
|
|
script_code = '''
|
|
import hashlib
|
|
import frappe
|
|
from frappe.utils.password import update_password, check_password
|
|
|
|
email = frappe.form_dict.get("email", "").strip().lower()
|
|
password = frappe.form_dict.get("password", "")
|
|
|
|
if not email or not password:
|
|
frappe.throw("Email et mot de passe requis", frappe.AuthenticationError)
|
|
|
|
if not frappe.db.exists("User", email):
|
|
frappe.throw("Identifiants invalides", frappe.AuthenticationError)
|
|
|
|
user = frappe.get_doc("User", email)
|
|
|
|
if not user.enabled:
|
|
frappe.throw("Compte désactivé", frappe.AuthenticationError)
|
|
|
|
legacy_hash = (user.get("legacy_password_md5") or "").strip()
|
|
authenticated = False
|
|
|
|
if legacy_hash:
|
|
input_md5 = hashlib.md5(password.encode("utf-8")).hexdigest()
|
|
|
|
if input_md5 == legacy_hash:
|
|
update_password(email, password, logout_all_sessions=False)
|
|
frappe.db.set_value("User", email, "legacy_password_md5", "", update_modified=False)
|
|
frappe.db.commit()
|
|
frappe.logger().info(f"Portal auth bridge: migrated password for {email}")
|
|
authenticated = True
|
|
else:
|
|
try:
|
|
check_password(email, password)
|
|
frappe.db.set_value("User", email, "legacy_password_md5", "", update_modified=False)
|
|
frappe.db.commit()
|
|
authenticated = True
|
|
except frappe.AuthenticationError:
|
|
frappe.throw("Mot de passe incorrect", frappe.AuthenticationError)
|
|
else:
|
|
try:
|
|
check_password(email, password)
|
|
authenticated = True
|
|
except frappe.AuthenticationError:
|
|
frappe.throw("Mot de passe incorrect", frappe.AuthenticationError)
|
|
|
|
if authenticated:
|
|
frappe.local.login_manager.login_as(email)
|
|
frappe.response["message"] = "OK"
|
|
frappe.response["user"] = email
|
|
frappe.response["full_name"] = user.full_name
|
|
'''
|
|
|
|
# Create or update the Server Script
|
|
if frappe.db.exists("Server Script", SCRIPT_NAME):
|
|
doc = frappe.get_doc("Server Script", SCRIPT_NAME)
|
|
doc.script = script_code
|
|
doc.save(ignore_permissions=True)
|
|
print(f" Updated Server Script: {SCRIPT_NAME}")
|
|
else:
|
|
doc = frappe.get_doc({
|
|
"doctype": "Server Script",
|
|
"name": SCRIPT_NAME,
|
|
"__newname": SCRIPT_NAME,
|
|
"script_type": "API",
|
|
"api_method": "portal_login",
|
|
"allow_guest": 1,
|
|
"script": script_code,
|
|
})
|
|
doc.insert(ignore_permissions=True)
|
|
print(f" Created Server Script: {SCRIPT_NAME}")
|
|
|
|
frappe.db.commit()
|
|
print(" Done! Endpoint available at: POST /api/method/portal_login")
|
|
print(" Params: email, password")
|
|
print(" Returns: { message: 'OK', user: '...', full_name: '...' }")
|