""" Demo users initialization — creates the three canonical @kevlarai.com accounts and links them to the KevlarAI institute in both Supabase and Neo4j. Idempotent: existing users are reused, stale .edu demo users are removed. Run via: python3 main.py --mode demo-users """ import os import requests import time from typing import Dict, Any from modules.logger_tool import initialise_logger logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) INSTITUTE_DB = "cc.institutes.6585bf916ae84d72ab54cddf3ba4e648" INSTITUTE_ID = "6585bf91-6ae8-4d72-ab54-cddf3ba4e648" DEMO_USERS = [ { "email": "kcar@kevlarai.com", "password": "KevlarAI2025!", "username": "kcar", "full_name": "Kevin Carroll", "display_name": "Kevin", "user_type": "teacher", "role": "school_admin", }, { "email": "teacher1@kevlarai.com", "password": "Teacher1@KevlarAI!", "username": "teacher1.kevlarai", "full_name": "Sarah Chen", "display_name": "Sarah", "user_type": "teacher", "role": "teacher", }, { "email": "teacher2@kevlarai.com", "password": "Teacher2@KevlarAI!", "username": "teacher2.kevlarai", "full_name": "Marcus Rodriguez", "display_name": "Marcus", "user_type": "teacher", "role": "teacher", }, ] def initialize_demo_users() -> Dict[str, Any]: """Create/refresh canonical @kevlarai.com demo users.""" from neo4j import GraphDatabase from modules.database.supabase.utils.client import SupabaseServiceRoleClient sb_client = SupabaseServiceRoleClient() supabase_url = os.environ["SUPABASE_URL"] service_key = os.environ["SERVICE_ROLE_KEY"] auth_headers = { "apikey": service_key, "Authorization": f"Bearer {service_key}", "Content-Type": "application/json", } def auth_get(path, params=None): r = requests.get(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers, params=params) r.raise_for_status() return r.json() def auth_post(path, data): return requests.post(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers, json=data) def auth_delete(path): return requests.delete(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers) def sb_upsert(table, data, on_conflict=None): params = {} if on_conflict: params["on_conflict"] = on_conflict hdrs = {**auth_headers, "Prefer": "resolution=merge-duplicates,return=representation"} r = requests.post(f"{supabase_url}/rest/v1/{table}", headers=hdrs, json=data, params=params) return r errors = [] # ── Step 1: delete stale .edu users ───────────────────────────────────── logger.info("Removing stale .edu demo users...") try: existing = auth_get("/users", params={"per_page": 100}).get("users", []) edu_users = [u for u in existing if u.get("email", "").endswith("@kevlarai.edu")] for u in edu_users: # Try direct delete first r = auth_delete(f"/users/{u['id']}") if r.status_code not in (200, 204): # Profile has dependent rows — clean via SQL _purge_profile_rows(supabase_url, service_key, u["id"]) auth_delete(f"/users/{u['id']}") logger.info(f" Removed: {u['email']}") except Exception as e: logger.warning(f" .edu cleanup warning: {e}") # ── Step 2: create @kevlarai.com users ─────────────────────────────────── logger.info("Creating @kevlarai.com demo users...") created_users = {} for spec in DEMO_USERS: email = spec["email"] all_users = auth_get("/users", params={"per_page": 100}).get("users", []) existing_user = next((u for u in all_users if u.get("email") == email), None) if existing_user: uid = existing_user["id"] logger.info(f" {email}: already exists [{uid[:8]}]") created_users[email] = {"id": uid, **spec} continue r = auth_post("/users", { "email": email, "password": spec["password"], "email_confirm": True, "user_metadata": { "username": spec["username"], "full_name": spec["full_name"], "display_name": spec["display_name"], "user_type": spec["user_type"], }, }) if r.status_code in (200, 201): uid = r.json()["id"] logger.info(f" {email}: created [{uid[:8]}]") created_users[email] = {"id": uid, **spec} else: msg = f"Failed to create {email}: {r.text[:200]}" logger.error(f" {msg}") errors.append(msg) time.sleep(0.3) # ── Step 3: upsert profiles ────────────────────────────────────────────── logger.info("Upserting profiles...") for spec in DEMO_USERS: u = created_users.get(spec["email"]) if not u: continue sb_upsert("profiles", { "id": u["id"], "email": spec["email"], "user_type": spec["user_type"], "username": spec["username"], "full_name": spec["full_name"], "display_name": spec["display_name"], "school_id": INSTITUTE_ID, "neo4j_sync_status": "pending", }, on_conflict="id") # ── Step 4: upsert memberships ─────────────────────────────────────────── logger.info("Upserting institute memberships...") for spec in DEMO_USERS: u = created_users.get(spec["email"]) if not u: continue sb_upsert("institute_memberships", { "profile_id": u["id"], "institute_id": INSTITUTE_ID, "role": spec["role"], }, on_conflict="profile_id,institute_id") # ── Step 5: Teacher nodes in Neo4j ─────────────────────────────────────── logger.info("Creating Neo4j Teacher nodes...") try: driver = GraphDatabase.driver("bolt://192.168.0.209:7687", auth=("neo4j", "&%N304j&%")) new_emails = {spec["email"] for spec in DEMO_USERS} with driver.session(database=INSTITUTE_DB) as s: # Remove stale Teacher nodes stale = s.run("MATCH (t:Teacher) WHERE NOT t.worker_email IN $emails RETURN t.worker_email as e, t.uuid_string as u", emails=list(new_emails)).data() for t in stale: s.run("MATCH (t:Teacher {uuid_string: $u}) DETACH DELETE t", u=t["u"]) logger.info(f" Removed stale Teacher: {t['e']}") # Remove duplicate Teacher nodes (same email, different UUID) for spec in DEMO_USERS: u = created_users.get(spec["email"]) if not u: continue dupes = s.run("MATCH (t:Teacher {worker_email: $e}) WHERE t.uuid_string <> $u RETURN t.uuid_string as uid", e=spec["email"], u=u["id"]).data() for d in dupes: s.run("MATCH (t:Teacher {uuid_string: $u}) DETACH DELETE t", u=d["uid"]) logger.info(f" Removed duplicate Teacher UUID {d['uid'][:8]} for {spec['email']}") # Upsert Teacher nodes for spec in DEMO_USERS: u = created_users.get(spec["email"]) if not u: continue s.run(""" MERGE (t:Teacher {uuid_string: $uuid}) SET t.worker_email = $email, t.worker_name = $name, t.unique_id = $uid, t.user_type = 'teacher', t.worker_type = 'teacher' """, uuid=u["id"], email=spec["email"], name=spec["full_name"], uid=u["id"]) logger.info(f" Teacher node: {spec['email']} [{u['id'][:8]}]") driver.close() except Exception as e: msg = f"Neo4j Teacher node setup failed: {e}" logger.error(msg) errors.append(msg) return { "success": len(errors) == 0, "created": list(created_users.keys()), "errors": errors, "message": "Demo users initialized" if not errors else f"{len(errors)} errors: {errors[0]}", } def _purge_profile_rows(supabase_url: str, service_key: str, profile_id: str) -> None: """Delete all rows referencing a profile before deleting the auth user.""" hdrs = {"apikey": service_key, "Authorization": f"Bearer {service_key}"} for table, col in [("files", "uploaded_by"), ("whiteboard_rooms", "user_id"), ("cabinet_memberships", "profile_id"), ("institute_memberships", "profile_id")]: requests.delete(f"{supabase_url}/rest/v1/{table}?{col}=eq.{profile_id}", headers=hdrs)