Backend services: - targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas, extract dispatch scoring weights, trim section dividers across 9 files - modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(), consolidate DM query factory, fix duplicate username fill bug, trim headers (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%) Frontend: - useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into 6 focused helpers (processOnlineStatus, processWanIPs, processRadios, processMeshNodes, processClients, checkRadioIssues) - EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments Documentation (17 → 13 files, -1,400 lines): - New consolidated README.md (architecture, services, dependencies, auth) - Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md - Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md - Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md - Update ROADMAP.md with current phase status - Delete CONTEXT.md (absorbed into README) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
"""
|
|
Create Website Users for customer portal + store legacy MD5 password hashes.
|
|
Bridge auth: on first login, verify MD5 → convert to pbkdf2 → clear legacy hash.
|
|
|
|
Requires custom field 'legacy_password_md5' (Data) on User doctype.
|
|
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/create_portal_users.py
|
|
"""
|
|
import os, sys, time
|
|
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)
|
|
|
|
os.chdir("/home/frappe/frappe-bench/sites")
|
|
import frappe
|
|
frappe.init(site="erp.gigafibre.ca", sites_path=".")
|
|
frappe.connect()
|
|
# Disable rate limiting for bulk user creation
|
|
frappe.flags.in_migrate = True
|
|
frappe.flags.in_install = True
|
|
print("Connected:", frappe.local.site)
|
|
|
|
import pymysql
|
|
|
|
DRY_RUN = False # SET TO False WHEN READY
|
|
|
|
# ── Step 1: Ensure custom field exists ──
|
|
print("\n" + "=" * 60)
|
|
print("Step 1: Ensure legacy_password_md5 custom field on User")
|
|
print("=" * 60)
|
|
|
|
if not frappe.db.exists('Custom Field', {'dt': 'User', 'fieldname': 'legacy_password_md5'}):
|
|
if not DRY_RUN:
|
|
cf = frappe.get_doc({
|
|
'doctype': 'Custom Field',
|
|
'dt': 'User',
|
|
'fieldname': 'legacy_password_md5',
|
|
'label': 'Legacy Password (MD5)',
|
|
'fieldtype': 'Data',
|
|
'hidden': 1,
|
|
'no_copy': 1,
|
|
'print_hide': 1,
|
|
'insert_after': 'last_password_reset_date',
|
|
})
|
|
cf.insert(ignore_permissions=True)
|
|
frappe.db.commit()
|
|
print(" Created custom field")
|
|
else:
|
|
print(" [DRY RUN] Would create custom field")
|
|
else:
|
|
print(" Custom field already exists")
|
|
|
|
# ── Step 2: Fetch legacy accounts with email + password ──
|
|
print("\n" + "=" * 60)
|
|
print("Step 2: Fetch legacy accounts")
|
|
print("=" * 60)
|
|
|
|
legacy = pymysql.connect(
|
|
host="legacy-db", user="facturation", password="VD67owoj",
|
|
database="gestionclient", cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
with legacy.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT id, email, password, first_name, last_name,
|
|
status, customer_id
|
|
FROM account
|
|
WHERE email IS NOT NULL AND email != '' AND TRIM(email) != ''
|
|
AND password IS NOT NULL AND password != ''
|
|
ORDER BY id
|
|
""")
|
|
legacy_accounts = cur.fetchall()
|
|
legacy.close()
|
|
|
|
print(f" Legacy accounts with email + password: {len(legacy_accounts)}")
|
|
|
|
# ── Step 3: Build legacy_account_id → Customer name map ──
|
|
acct_to_cust = {}
|
|
rows = frappe.db.sql('SELECT name, legacy_account_id FROM "tabCustomer" WHERE legacy_account_id IS NOT NULL', as_dict=True)
|
|
for r in rows:
|
|
acct_to_cust[int(r['legacy_account_id'])] = r['name']
|
|
print(f" Customers with legacy_account_id: {len(acct_to_cust)}")
|
|
|
|
# ── Step 4: Check existing users ──
|
|
existing_users = set()
|
|
rows = frappe.db.sql('SELECT LOWER(name) as name FROM "tabUser"', as_dict=True)
|
|
for r in rows:
|
|
existing_users.add(r['name'])
|
|
|
|
print(f" Existing ERPNext users: {len(existing_users)}")
|
|
|
|
# ── Step 5: Create Website Users ──
|
|
print("\n" + "=" * 60)
|
|
print("Step 3: Create Website Users")
|
|
print("=" * 60)
|
|
|
|
t0 = time.time()
|
|
created = 0
|
|
skipped_existing = 0
|
|
skipped_no_customer = 0
|
|
skipped_bad_email = 0
|
|
errors = 0
|
|
|
|
for acc in legacy_accounts:
|
|
email = acc['email'].strip().lower()
|
|
|
|
# Basic email validation
|
|
if '@' not in email or '.' not in email.split('@')[-1]:
|
|
skipped_bad_email += 1
|
|
continue
|
|
|
|
# Already exists?
|
|
if email in existing_users:
|
|
# Just update the legacy hash if not already set
|
|
if not DRY_RUN and acc['password']:
|
|
try:
|
|
frappe.db.sql(
|
|
"""UPDATE "tabUser" SET legacy_password_md5 = %s
|
|
WHERE LOWER(name) = %s AND (legacy_password_md5 IS NULL OR legacy_password_md5 = '')""",
|
|
(acc['password'], email)
|
|
)
|
|
except Exception:
|
|
pass # Field might not exist yet in dry run
|
|
skipped_existing += 1
|
|
continue
|
|
|
|
# Find the ERPNext customer via legacy_account_id
|
|
cust_name = acct_to_cust.get(int(acc['id']))
|
|
|
|
if not cust_name:
|
|
skipped_no_customer += 1
|
|
continue
|
|
|
|
first_name = (acc.get('first_name') or '').strip() or 'Client'
|
|
last_name = (acc.get('last_name') or '').strip() or ''
|
|
full_name = f"{first_name} {last_name}".strip()
|
|
|
|
if not DRY_RUN:
|
|
try:
|
|
user = frappe.get_doc({
|
|
'doctype': 'User',
|
|
'email': email,
|
|
'first_name': first_name,
|
|
'last_name': last_name,
|
|
'full_name': full_name,
|
|
'enabled': 1,
|
|
'user_type': 'Website User',
|
|
'legacy_password_md5': acc['password'] or '',
|
|
'roles': [{'role': 'Customer'}],
|
|
})
|
|
user.flags.no_welcome_email = True # Don't send email yet
|
|
user.flags.ignore_permissions = True
|
|
user.flags.in_import = True
|
|
user.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
|
created += 1
|
|
|
|
# Link user to customer as portal user
|
|
customer_doc = frappe.get_doc('Customer', cust_name)
|
|
customer_doc.append('portal_users', {'user': email})
|
|
customer_doc.save(ignore_permissions=True)
|
|
|
|
except frappe.DuplicateEntryError:
|
|
skipped_existing += 1
|
|
except Exception as e:
|
|
errors += 1
|
|
if errors <= 10:
|
|
print(f" ERR {email}: {e}")
|
|
else:
|
|
created += 1 # Count as would-create
|
|
|
|
if (created + skipped_existing) % 1000 == 0:
|
|
if not DRY_RUN:
|
|
frappe.db.commit()
|
|
elapsed = time.time() - t0
|
|
total = created + skipped_existing + skipped_no_customer + skipped_bad_email + errors
|
|
print(f" Progress: {total}/{len(legacy_accounts)} created={created} existing={skipped_existing} [{elapsed:.0f}s]")
|
|
|
|
if not DRY_RUN:
|
|
frappe.db.commit()
|
|
|
|
elapsed = time.time() - t0
|
|
print(f"\n Created: {created}")
|
|
print(f" Already existed: {skipped_existing}")
|
|
print(f" No matching customer: {skipped_no_customer}")
|
|
print(f" Bad email: {skipped_bad_email}")
|
|
print(f" Errors: {errors}")
|
|
print(f" Time: {elapsed:.0f}s")
|
|
|
|
if DRY_RUN:
|
|
print("\n ** DRY RUN — no changes made **")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("DONE")
|
|
print("=" * 60)
|