1+ import asyncio
2+ import logging
13from typing import Any
24
35import strawberry
46from auth import get_user_payload
5- from database .models .user import User
7+ from database .models .location import LocationNode , location_organizations
8+ from database .models .user import User , user_root_locations
69from database .session import get_db_session
710from fastapi import Depends
8- from sqlalchemy import select
11+ from graphql import GraphQLError
12+ from sqlalchemy import delete , select
13+ from sqlalchemy .dialects .postgresql import insert
914from sqlalchemy .exc import IntegrityError
1015from sqlalchemy .ext .asyncio import AsyncSession
1116from starlette .requests import HTTPConnection
1217from strawberry .fastapi import BaseContext
1318
1419
20+ class LockedAsyncSession :
21+ def __init__ (self , session : AsyncSession , lock : asyncio .Lock ):
22+ self ._session = session
23+ self ._lock = lock
24+
25+ async def execute (self , * args , ** kwargs ):
26+ async with self ._lock :
27+ return await self ._session .execute (* args , ** kwargs )
28+
29+ async def commit (self , * args , ** kwargs ):
30+ async with self ._lock :
31+ return await self ._session .commit (* args , ** kwargs )
32+
33+ async def rollback (self , * args , ** kwargs ):
34+ async with self ._lock :
35+ return await self ._session .rollback (* args , ** kwargs )
36+
37+ async def flush (self , * args , ** kwargs ):
38+ async with self ._lock :
39+ return await self ._session .flush (* args , ** kwargs )
40+
41+ async def refresh (self , * args , ** kwargs ):
42+ async with self ._lock :
43+ return await self ._session .refresh (* args , ** kwargs )
44+
45+ def add (self , * args , ** kwargs ):
46+ return self ._session .add (* args , ** kwargs )
47+
48+ def __getattr__ (self , name ):
49+ return getattr (self ._session , name )
50+
51+
1552class Context (BaseContext ):
16- def __init__ (self , db : AsyncSession , user : "User | None" = None ):
53+ def __init__ (self , db : AsyncSession , user : "User | None" = None , organizations : str | None = None ):
1754 super ().__init__ ()
18- self .db = db
55+ self ._db = db
1956 self .user = user
57+ self .organizations = organizations
58+ self ._accessible_location_ids : set [str ] | None = None
59+ self ._accessible_location_ids_lock = asyncio .Lock ()
60+ self ._db_lock = asyncio .Lock ()
61+ self .db = LockedAsyncSession (db , self ._db_lock )
2062
2163
2264Info = strawberry .Info [Context , Any ]
@@ -28,6 +70,7 @@ async def get_context(
2870) -> Context :
2971 user_payload = get_user_payload (connection )
3072 db_user = None
73+ organizations = None
3174
3275 if user_payload :
3376 user_id = user_payload .get ("sub" )
@@ -39,13 +82,27 @@ async def get_context(
3982 email = user_payload .get ("email" )
4083 picture = user_payload .get ("picture" )
4184
85+ logger = logging .getLogger (__name__ )
4286 organizations_raw = user_payload .get ("organization" )
87+
88+ if organizations_raw is None :
89+ scope = user_payload .get ("scope" , "" )
90+ has_org_scope = "organization" in scope .split () if scope else False
91+ logger .warning (
92+ f"Organization claim not found in token for user { user_payload .get ('sub' , 'unknown' )} . "
93+ f"Has organization scope: { has_org_scope } . "
94+ f"Token scope: { scope } . "
95+ f"Available claims: { sorted (user_payload .keys ())} "
96+ )
97+
4398 organizations = None
4499 if organizations_raw :
45100 if isinstance (organizations_raw , list ):
46- organizations = "," .join (str (org ) for org in organizations_raw )
101+ org_list = [str (org ) for org in organizations_raw if org ]
102+ if org_list :
103+ organizations = "," .join (org_list )
47104 else :
48- organizations = str (organizations_raw )
105+ organizations = str (organizations_raw ) if organizations_raw else None
49106
50107 if user_id :
51108 result = await session .execute (
@@ -63,7 +120,6 @@ async def get_context(
63120 lastname = lastname ,
64121 title = "User" ,
65122 avatar_url = picture ,
66- organizations = organizations ,
67123 )
68124 session .add (new_user )
69125 await session .commit ()
@@ -75,24 +131,144 @@ async def get_context(
75131 select (User ).where (User .id == user_id ),
76132 )
77133 db_user = result .scalars ().first ()
134+ except Exception as e :
135+ await session .rollback ()
136+ raise GraphQLError (
137+ "Failed to create user. Please contact an administrator if you believe this is an error." ,
138+ extensions = {"code" : "INTERNAL_SERVER_ERROR" },
139+ ) from e
78140
79141 if db_user and (
80142 db_user .username != username
81143 or db_user .firstname != firstname
82144 or db_user .lastname != lastname
83145 or db_user .email != email
84146 or db_user .avatar_url != picture
85- or db_user .organizations != organizations
86147 ):
87148 db_user .username = username
88149 db_user .firstname = firstname
89150 db_user .lastname = lastname
90151 db_user .email = email
91152 if picture :
92153 db_user .avatar_url = picture
93- db_user .organizations = organizations
94154 session .add (db_user )
95155 await session .commit ()
96156 await session .refresh (db_user )
97157
98- return Context (db = session , user = db_user )
158+ if db_user :
159+ try :
160+
161+ await _update_user_root_locations (
162+ session ,
163+ db_user ,
164+ organizations ,
165+ )
166+ except Exception as e :
167+ raise GraphQLError (
168+ "Failed to update user root locations. Please contact an administrator if you believe this is an error." ,
169+ extensions = {"code" : "INTERNAL_SERVER_ERROR" },
170+ ) from e
171+
172+ return Context (db = session , user = db_user , organizations = organizations )
173+
174+
175+ async def _update_user_root_locations (
176+ session : AsyncSession ,
177+ user : User ,
178+ organizations : str | None ,
179+ ) -> None :
180+ organization_ids : list [str ] = []
181+ if organizations :
182+ organization_ids = [
183+ org_id .strip ()
184+ for org_id in organizations .split ("," )
185+ if org_id .strip ()
186+ ]
187+
188+ root_location_ids : list [str ] = []
189+
190+ if organization_ids :
191+ result = await session .execute (
192+ select (LocationNode )
193+ .join (
194+ location_organizations ,
195+ LocationNode .id == location_organizations .c .location_id ,
196+ )
197+ .where (
198+ location_organizations .c .organization_id .in_ (organization_ids ),
199+ ),
200+ )
201+ found_locations = result .scalars ().all ()
202+ root_location_ids = [loc .id for loc in found_locations ]
203+
204+ found_org_ids = set ()
205+ for loc in found_locations :
206+ org_result = await session .execute (
207+ select (location_organizations .c .organization_id ).where (
208+ location_organizations .c .location_id == loc .id ,
209+ location_organizations .c .organization_id .in_ (
210+ organization_ids ,
211+ ),
212+ ),
213+ )
214+ found_org_ids .update (row [0 ] for row in org_result .all ())
215+
216+ for org_id in organization_ids :
217+ if org_id not in found_org_ids :
218+ new_location = LocationNode (
219+ title = f"Organization { org_id [:8 ]} " ,
220+ kind = "CLINIC" ,
221+ parent_id = None ,
222+ )
223+ session .add (new_location )
224+ await session .flush ()
225+ await session .refresh (new_location )
226+
227+ await session .execute (
228+ location_organizations .insert ().values (
229+ location_id = new_location .id ,
230+ organization_id = org_id ,
231+ ),
232+ )
233+ root_location_ids .append (new_location .id )
234+
235+ if not root_location_ids :
236+ personal_org_title = f"{ user .username } 's Organization"
237+ result = await session .execute (
238+ select (LocationNode ).where (
239+ LocationNode .title == personal_org_title ,
240+ LocationNode .parent_id .is_ (None ),
241+ ),
242+ )
243+ personal_location = result .scalars ().first ()
244+
245+ if not personal_location :
246+ personal_location = LocationNode (
247+ title = personal_org_title ,
248+ kind = "CLINIC" ,
249+ parent_id = None ,
250+ )
251+ session .add (personal_location )
252+ await session .flush ()
253+
254+ root_location_ids = [personal_location .id ]
255+
256+ await session .execute (
257+ delete (user_root_locations ).where (
258+ user_root_locations .c .user_id == user .id ,
259+ ),
260+ )
261+
262+ if root_location_ids :
263+ stmt = insert (user_root_locations ).values (
264+ [
265+ {"user_id" : user .id , "location_id" : loc_id }
266+ for loc_id in root_location_ids
267+ ],
268+ )
269+ stmt = stmt .on_conflict_do_nothing (
270+ index_elements = ["user_id" , "location_id" ],
271+ )
272+ await session .execute (stmt )
273+
274+ await session .commit ()
0 commit comments