api/routers/database/tools/user_init_router.py
kcar fe3d7a12c8 feat(phase-b): school/timetable API routers + graph nav tree
New routers (all previously untracked):
- graph_tree_router: /graph/tree, /graph/node/children, /graph/calendar/academic
  Supabase-driven tree builder; institute DB resolved by teacher email scan
- school_router: /school/status (role + calendar flags), /school/info PATCH
  Self-heals profiles.school_id from institute_memberships if null
- timetable_builder_router: /timetable/setup (AcademicYear/Term/Week + SchoolTimetable),
  /timetable/slots (read/write TimetableSlot nodes), /timetable/init (TeacherTimetable)
- user_init_router: /user/init (provision user node in institute DB)

routers.py: register all new routers with correct prefixes
users.py: add JournalNode and PlannerNode schema classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 01:24:44 +01:00

82 lines
3.4 KiB
Python

import os
from typing import Dict, Any
from fastapi import APIRouter, Depends
from modules.logger_tool import initialise_logger
from modules.auth.supabase_bearer import SupabaseBearer
from modules.database.services.provisioning_service import ProvisioningService
import modules.database.tools.neo4j_driver_tools as driver_tools
import modules.database.schemas.nodes.users as user_nodes
import modules.database.tools.neontology_tools as neon
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
router = APIRouter()
def _teacher_db(user_id: str) -> str:
return f"cc.users.teacher.{user_id.replace('-', '')}"
def _ensure_journal_planner(user_id: str, teacher_db: str) -> None:
neon.init_neontology_connection()
try:
with driver_tools.get_session(database=teacher_db) as session:
has_journal = session.run("MATCH (j:Journal) RETURN count(j) AS n").single()["n"] > 0
has_planner = session.run("MATCH (p:Planner) RETURN count(p) AS n").single()["n"] > 0
if not has_journal:
journal = user_nodes.JournalNode(
uuid_string=f"{user_id}_journal",
node_storage_path=f"users/{user_id}/nodes/journal",
user_id=user_id,
)
neon.create_or_merge_neontology_node(journal, database=teacher_db, operation='merge')
logger.info(f"Created Journal node for {user_id}")
if not has_planner:
planner = user_nodes.PlannerNode(
uuid_string=f"{user_id}_planner",
node_storage_path=f"users/{user_id}/nodes/planner",
user_id=user_id,
)
neon.create_or_merge_neontology_node(planner, database=teacher_db, operation='merge')
logger.info(f"Created Planner node for {user_id}")
finally:
neon.close_neontology_connection()
@router.post("/init")
async def init_user(credentials: dict = Depends(SupabaseBearer())) -> Dict[str, Any]:
user_id = credentials.get("sub", "")
if not user_id:
return {"status": "error", "message": "No user ID in token"}
db = _teacher_db(user_id)
# Fast path: check if already fully provisioned
try:
with driver_tools.get_session(database=db) as session:
r = session.run(
"MATCH (u:User) "
"OPTIONAL MATCH (j:Journal) "
"OPTIONAL MATCH (p:Planner) "
"RETURN count(u) AS u, count(j) AS j, count(p) AS p"
).single()
if r and r["u"] > 0 and r["j"] > 0 and r["p"] > 0:
logger.debug(f"User {user_id} already initialized — fast path")
return {"status": "ok", "initialized": True, "teacher_db": db}
except Exception:
pass # DB doesn't exist yet — fall through to full provisioning
# Full provisioning
try:
logger.info(f"Provisioning user {user_id}...")
service = ProvisioningService()
result = service.ensure_user(user_id)
user_db = result.get("user_db_name") or db
_ensure_journal_planner(user_id, user_db)
logger.info(f"User {user_id} provisioned successfully: {user_db}")
return {"status": "ok", "initialized": True, "teacher_db": user_db}
except Exception as e:
logger.error(f"User init failed for {user_id}: {e}")
return {"status": "error", "message": str(e)}