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>
82 lines
3.4 KiB
Python
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)}
|