feat(phase-b): rewrite demo_users with initialize_demo_users() for clean restart
Wraps all logic in initialize_demo_users() matching __init__.py import. Idempotent: deletes stale .edu users, creates 3 @kevlarai.com demo accounts, upserts Supabase profiles + institute_memberships, syncs Teacher nodes in Neo4j.
This commit is contained in:
parent
e42cd09dea
commit
7c75481245
@ -1,395 +1,218 @@
|
|||||||
"""
|
"""
|
||||||
Demo users initialization module for ClassroomCopilot
|
Demo users initialization — creates the three canonical @kevlarai.com accounts
|
||||||
Creates demo teachers and students
|
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 os
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.logger_tool import initialise_logger
|
from modules.logger_tool import initialise_logger
|
||||||
from modules.database.services.provisioning_service import ProvisioningService
|
|
||||||
|
|
||||||
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
|
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
|
||||||
|
|
||||||
class DemoUsersInitializer:
|
INSTITUTE_DB = "cc.institutes.6585bf916ae84d72ab54cddf3ba4e648"
|
||||||
"""Handles demo users creation"""
|
INSTITUTE_ID = "6585bf91-6ae8-4d72-ab54-cddf3ba4e648"
|
||||||
|
|
||||||
def __init__(self, supabase_url: str, service_role_key: str):
|
DEMO_USERS = [
|
||||||
self.supabase_url = supabase_url
|
{
|
||||||
self.service_role_key = service_role_key
|
"email": "kcar@kevlarai.com",
|
||||||
self.supabase_headers = {
|
"password": "KevlarAI2025!",
|
||||||
"apikey": service_role_key,
|
"username": "kcar",
|
||||||
"Authorization": f"Bearer {service_role_key}",
|
"full_name": "Kevin Carroll",
|
||||||
"Content-Type": "application/json"
|
"display_name": "Kevin",
|
||||||
}
|
"user_type": "teacher",
|
||||||
self.provisioning_service = ProvisioningService()
|
"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 create_demo_users(self) -> Dict[str, Any]:
|
|
||||||
"""Create demo teachers and students"""
|
|
||||||
logger.info("Creating demo users...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Define demo users
|
|
||||||
demo_users = [
|
|
||||||
# Demo Teachers
|
|
||||||
{
|
|
||||||
"email": "teacher1@kevlarai.edu",
|
|
||||||
"password": "DemoTeacher123!",
|
|
||||||
"email_confirm": True,
|
|
||||||
"user_metadata": {
|
|
||||||
"name": "Dr. Sarah Chen",
|
|
||||||
"username": "sarah.chen",
|
|
||||||
"full_name": "Dr. Sarah Chen",
|
|
||||||
"display_name": "Dr. Chen",
|
|
||||||
"user_type": "teacher"
|
|
||||||
},
|
|
||||||
"app_metadata": {
|
|
||||||
"provider": "email",
|
|
||||||
"providers": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"email": "teacher2@kevlarai.edu",
|
|
||||||
"password": "DemoTeacher123!",
|
|
||||||
"email_confirm": True,
|
|
||||||
"user_metadata": {
|
|
||||||
"name": "Prof. Marcus Rodriguez",
|
|
||||||
"username": "marcus.rodriguez",
|
|
||||||
"full_name": "Professor Marcus Rodriguez",
|
|
||||||
"display_name": "Prof. Rodriguez",
|
|
||||||
"user_type": "teacher"
|
|
||||||
},
|
|
||||||
"app_metadata": {
|
|
||||||
"provider": "email",
|
|
||||||
"providers": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Demo Students
|
|
||||||
{
|
|
||||||
"email": "student1@kevlarai.edu",
|
|
||||||
"password": "DemoStudent123!",
|
|
||||||
"email_confirm": True,
|
|
||||||
"user_metadata": {
|
|
||||||
"name": "Alex Thompson",
|
|
||||||
"username": "alex.thompson",
|
|
||||||
"full_name": "Alex Thompson",
|
|
||||||
"display_name": "Alex",
|
|
||||||
"user_type": "student"
|
|
||||||
},
|
|
||||||
"app_metadata": {
|
|
||||||
"provider": "email",
|
|
||||||
"providers": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"email": "student2@kevlarai.edu",
|
|
||||||
"password": "DemoStudent123!",
|
|
||||||
"email_confirm": True,
|
|
||||||
"user_metadata": {
|
|
||||||
"name": "Jordan Lee",
|
|
||||||
"username": "jordan.lee",
|
|
||||||
"full_name": "Jordan Lee",
|
|
||||||
"display_name": "Jordan",
|
|
||||||
"user_type": "student"
|
|
||||||
},
|
|
||||||
"app_metadata": {
|
|
||||||
"provider": "email",
|
|
||||||
"providers": ["email"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
created_users = []
|
|
||||||
failed_users = []
|
|
||||||
|
|
||||||
for user_data in demo_users:
|
|
||||||
try:
|
|
||||||
# Create user via Auth API
|
|
||||||
response = self._supabase_request_with_retry(
|
|
||||||
'post',
|
|
||||||
f"{self.supabase_url}/auth/v1/admin/users",
|
|
||||||
headers=self.supabase_headers,
|
|
||||||
json=user_data
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code in (200, 201):
|
|
||||||
user = response.json()
|
|
||||||
user_id = user.get("id")
|
|
||||||
|
|
||||||
# Wait a moment for user to be created
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Create profile
|
|
||||||
profile_data = {
|
|
||||||
"id": user_id,
|
|
||||||
"email": user_data["email"],
|
|
||||||
"user_type": user_data["user_metadata"]["user_type"],
|
|
||||||
"username": user_data["user_metadata"]["username"],
|
|
||||||
"full_name": user_data["user_metadata"]["full_name"],
|
|
||||||
"display_name": user_data["user_metadata"]["display_name"]
|
|
||||||
}
|
|
||||||
|
|
||||||
profile_response = self._supabase_request_with_retry(
|
|
||||||
'post',
|
|
||||||
f"{self.supabase_url}/rest/v1/profiles",
|
|
||||||
headers=self.supabase_headers,
|
|
||||||
json=profile_data
|
|
||||||
)
|
|
||||||
|
|
||||||
if profile_response.status_code in (200, 201):
|
|
||||||
created_users.append({
|
|
||||||
"id": user_id,
|
|
||||||
"email": user_data["email"],
|
|
||||||
"user_type": user_data["user_metadata"]["user_type"],
|
|
||||||
"username": user_data["user_metadata"]["username"]
|
|
||||||
})
|
|
||||||
logger.info(f"Successfully created user: {user_data['email']}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Failed to create profile for {user_data['email']}: {profile_response.text}")
|
|
||||||
failed_users.append({
|
|
||||||
"email": user_data["email"],
|
|
||||||
"error": f"Profile creation failed: {profile_response.text}"
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
logger.warning(f"Failed to create user {user_data['email']}: {response.text}")
|
|
||||||
failed_users.append({
|
|
||||||
"email": user_data["email"],
|
|
||||||
"error": f"User creation failed: {response.text}"
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating user {user_data['email']}: {str(e)}")
|
|
||||||
failed_users.append({
|
|
||||||
"email": user_data["email"],
|
|
||||||
"error": str(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Create institute memberships for KevlarAI and provision users
|
|
||||||
all_users_to_provision = []
|
|
||||||
|
|
||||||
# Add newly created users
|
|
||||||
if created_users:
|
|
||||||
all_users_to_provision.extend(created_users)
|
|
||||||
self._create_institute_memberships(created_users)
|
|
||||||
|
|
||||||
# Also provision existing users that failed due to email_exists
|
|
||||||
existing_users = []
|
|
||||||
for failed_user in failed_users:
|
|
||||||
if "email_exists" in failed_user.get("error", ""):
|
|
||||||
# Get the existing user ID from Supabase
|
|
||||||
existing_user_id = self._get_existing_user_id(failed_user["email"])
|
|
||||||
if existing_user_id:
|
|
||||||
existing_users.append({
|
|
||||||
"id": existing_user_id,
|
|
||||||
"email": failed_user["email"],
|
|
||||||
"user_type": self._get_user_type_from_email(failed_user["email"]),
|
|
||||||
"username": self._get_username_from_email(failed_user["email"])
|
|
||||||
})
|
|
||||||
|
|
||||||
if existing_users:
|
|
||||||
logger.info(f"Found {len(existing_users)} existing users to provision")
|
|
||||||
all_users_to_provision.extend(existing_users)
|
|
||||||
self._create_institute_memberships(existing_users)
|
|
||||||
|
|
||||||
# Provision all users (new and existing)
|
|
||||||
if all_users_to_provision:
|
|
||||||
self._provision_users(all_users_to_provision)
|
|
||||||
|
|
||||||
logger.info(f"Demo users creation completed: {len(created_users)} created, {len(failed_users)} failed")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": f"Successfully created {len(created_users)} demo users",
|
|
||||||
"created_users": created_users,
|
|
||||||
"failed_users": failed_users
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating demo users: {str(e)}")
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"Error creating demo users: {str(e)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def _create_institute_memberships(self, users: list) -> None:
|
|
||||||
"""Create institute memberships for users in KevlarAI"""
|
|
||||||
logger.info("Creating institute memberships for demo users...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get KevlarAI institute ID
|
|
||||||
response = self._supabase_request_with_retry(
|
|
||||||
'get',
|
|
||||||
f"{self.supabase_url}/rest/v1/institutes",
|
|
||||||
headers=self.supabase_headers,
|
|
||||||
params={
|
|
||||||
"select": "id",
|
|
||||||
"name": "eq.KevlarAI"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
logger.warning("Could not get KevlarAI institute ID for memberships")
|
|
||||||
return
|
|
||||||
|
|
||||||
institutes = response.json()
|
|
||||||
if not institutes:
|
|
||||||
logger.warning("KevlarAI institute not found for memberships")
|
|
||||||
return
|
|
||||||
|
|
||||||
institute_id = institutes[0]["id"]
|
|
||||||
|
|
||||||
# Get user profile IDs
|
|
||||||
for user in users:
|
|
||||||
try:
|
|
||||||
profile_response = self._supabase_request_with_retry(
|
|
||||||
'get',
|
|
||||||
f"{self.supabase_url}/rest/v1/profiles",
|
|
||||||
headers=self.supabase_headers,
|
|
||||||
params={
|
|
||||||
"select": "id",
|
|
||||||
"email": f"eq.{user['email']}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if profile_response.status_code == 200:
|
|
||||||
profiles = profile_response.json()
|
|
||||||
if profiles:
|
|
||||||
profile_id = profiles[0]["id"]
|
|
||||||
|
|
||||||
# Create membership
|
|
||||||
membership_data = {
|
|
||||||
"profile_id": profile_id,
|
|
||||||
"institute_id": institute_id,
|
|
||||||
"role": user["user_type"]
|
|
||||||
}
|
|
||||||
|
|
||||||
membership_response = self._supabase_request_with_retry(
|
|
||||||
'post',
|
|
||||||
f"{self.supabase_url}/rest/v1/institute_memberships",
|
|
||||||
headers=self.supabase_headers,
|
|
||||||
json=membership_data
|
|
||||||
)
|
|
||||||
|
|
||||||
if membership_response.status_code in (200, 201):
|
|
||||||
logger.info(f"Created membership for {user['email']} in KevlarAI")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Failed to create membership for {user['email']}: {membership_response.text}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error creating membership for {user['email']}: {str(e)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error creating institute memberships: {str(e)}")
|
|
||||||
|
|
||||||
def _get_existing_user_id(self, email: str) -> str:
|
|
||||||
"""Get the user ID for an existing user by email"""
|
|
||||||
try:
|
|
||||||
response = self._supabase_request_with_retry(
|
|
||||||
'get',
|
|
||||||
f"{self.supabase_url}/rest/v1/profiles",
|
|
||||||
headers=self.supabase_headers,
|
|
||||||
params={
|
|
||||||
"select": "id",
|
|
||||||
"email": f"eq.{email}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
profiles = response.json()
|
|
||||||
if profiles and len(profiles) > 0:
|
|
||||||
return profiles[0].get("id")
|
|
||||||
|
|
||||||
logger.warning(f"Could not find existing user ID for {email}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error getting existing user ID for {email}: {str(e)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_user_type_from_email(self, email: str) -> str:
|
|
||||||
"""Get user type from email based on demo user definitions"""
|
|
||||||
if "teacher" in email:
|
|
||||||
return "teacher"
|
|
||||||
elif "student" in email:
|
|
||||||
return "student"
|
|
||||||
return "teacher" # default
|
|
||||||
|
|
||||||
def _get_username_from_email(self, email: str) -> str:
|
|
||||||
"""Get username from email based on demo user definitions"""
|
|
||||||
username_map = {
|
|
||||||
"teacher1@kevlarai.edu": "sarah.chen",
|
|
||||||
"teacher2@kevlarai.edu": "marcus.rodriguez",
|
|
||||||
"student1@kevlarai.edu": "alex.thompson",
|
|
||||||
"student2@kevlarai.edu": "jordan.lee"
|
|
||||||
}
|
|
||||||
return username_map.get(email, email.split("@")[0])
|
|
||||||
|
|
||||||
def _provision_users(self, users: list) -> None:
|
|
||||||
"""Provision Neo4j databases for the created demo users."""
|
|
||||||
for user in users:
|
|
||||||
user_id = user.get("id")
|
|
||||||
if not user_id:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
self.provisioning_service.ensure_user(user_id)
|
|
||||||
logger.info(f"Provisioned Neo4j resources for {user.get('email')}")
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning(f"Failed to provision Neo4j resources for {user.get('email')}: {exc}")
|
|
||||||
|
|
||||||
def _supabase_request_with_retry(self, method, url, **kwargs):
|
|
||||||
"""Make a request to Supabase with retry logic"""
|
|
||||||
max_retries = 3
|
|
||||||
retry_delay = 2 # seconds
|
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
if method.lower() == 'get':
|
|
||||||
response = requests.get(url, **kwargs)
|
|
||||||
elif method.lower() == 'post':
|
|
||||||
response = requests.post(url, **kwargs)
|
|
||||||
elif method.lower() == 'put':
|
|
||||||
response = requests.put(url, **kwargs)
|
|
||||||
elif method.lower() == 'delete':
|
|
||||||
response = requests.delete(url, **kwargs)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
||||||
|
|
||||||
# If successful or client error (4xx), don't retry
|
|
||||||
if response.status_code < 500:
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Server error (5xx), retry after delay
|
|
||||||
logger.warning(f"Supabase server error (attempt {attempt+1}/{max_retries}): {response.status_code} - {response.text}")
|
|
||||||
time.sleep(retry_delay * (attempt + 1)) # Exponential backoff
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
logger.warning(f"Supabase request exception (attempt {attempt+1}/{max_retries}): {str(e)}")
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
raise
|
|
||||||
time.sleep(retry_delay * (attempt + 1))
|
|
||||||
|
|
||||||
# If we get here, all retries failed with server errors
|
|
||||||
raise requests.RequestException(f"Failed after {max_retries} attempts to {method} {url}")
|
|
||||||
|
|
||||||
def initialize_demo_users() -> Dict[str, Any]:
|
def initialize_demo_users() -> Dict[str, Any]:
|
||||||
"""Initialize demo users"""
|
"""Create/refresh canonical @kevlarai.com demo users."""
|
||||||
logger.info("Starting demo users initialization...")
|
from neo4j import GraphDatabase
|
||||||
|
from modules.database.supabase.utils.client import SupabaseServiceRoleClient
|
||||||
|
|
||||||
supabase_url = os.getenv("SUPABASE_URL")
|
sb_client = SupabaseServiceRoleClient()
|
||||||
service_role_key = os.getenv("SERVICE_ROLE_KEY")
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
if not supabase_url or not service_role_key:
|
def auth_get(path, params=None):
|
||||||
return {"success": False, "message": "Missing SUPABASE_URL or SERVICE_ROLE_KEY environment variables"}
|
r = requests.get(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers, params=params)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
initializer = DemoUsersInitializer(supabase_url, service_role_key)
|
def auth_post(path, data):
|
||||||
|
return requests.post(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers, json=data)
|
||||||
|
|
||||||
# Create demo users
|
def auth_delete(path):
|
||||||
result = initializer.create_demo_users()
|
return requests.delete(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers)
|
||||||
|
|
||||||
if result["success"]:
|
def sb_upsert(table, data, on_conflict=None):
|
||||||
logger.info("Demo users initialization completed successfully")
|
params = {}
|
||||||
else:
|
if on_conflict:
|
||||||
logger.error(f"Demo users initialization failed: {result['message']}")
|
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
|
||||||
|
|
||||||
return result
|
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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user