
    Ҫi=                        d Z ddlZddlZddlmZmZmZ ddlmZmZm	Z	 ddl
mZ ddlZddlZddlZej                  j!                  d       ddlmZmZ ddlmZ  ej,                  e      Zd	d
dddZe G d d             Z G d d      Zy)an  
GHL OAuth Token Vault

Handles encrypted storage, retrieval, and lifecycle management of OAuth tokens.
Uses PostgreSQL for persistence and Redis for distributed locking during refresh.

Token types:
- 'agency': The main OAuth token from app installation. Has refresh_token.
- 'location': Per-sub-account tokens exchanged via /oauth/locationToken. No refresh_token.
    N)datetimetimezone	timedelta)OptionalDictAny)	dataclassz)/mnt/e/genesis-system/data/genesis-memory)PostgresConfigRedisConfig)GHLOAuthConfig      
      )
keepaliveskeepalives_idlekeepalives_intervalkeepalives_countc                       e Zd ZU dZeed<   eed<   ee   ed<   ee   ed<   eed<   ee   ed<   eed<   eed	<   ee   ed
<   ee   ed<   e	de
fd       Ze	de
fd       Ze	de
fd       Zy)TokenRecordzA single OAuth token record.id
token_typelocation_idlocation_nameaccess_tokenrefresh_tokenscopes
expires_atlast_used_at
revoked_atreturnc                 b    t        j                  t        j                        | j                  k\  S N)r   nowr   utcr   selfs    ./mnt/e/genesis-system/GHL/oauth/token_vault.py
is_expiredzTokenRecord.is_expired3   s    ||HLL)T__<<    c                     t        t        j                        }t        j                  t
        j                        | j                  |z
  k\  S )Nseconds)r   r   refresh_buffer_secondsr   r$   r   r%   r   )r'   buffers     r(   needs_refreshzTokenRecord.needs_refresh7   s3    >#H#HI||HLL)doo.FGGr*   c                 <    | j                    xr | j                  d u S r#   )r)   r    r&   s    r(   is_validzTokenRecord.is_valid<   s    ??">t$'>>r*   N)__name__
__module____qualname____doc__str__annotations__r   listr   propertyboolr)   r0   r2    r*   r(   r   r   %   s    &GO#C= C= L8$$""=D = = Ht H H ?$ ? ?r*   r   c                   V   e Zd ZdZd dee   fdZd Zd Zd Z	de
d	e
d
edede
f
dZde
de
de
d
edede
fdZdee   fdZde
dee   fdZdefdZdee   fdZd!de
de
dee   fdZde
dee
   fdZdedee
ef   fdZdee
ef   fdZde
fdZde
dee
   defdZedefd       Zy)"
TokenVaultzLManages GHL OAuth tokens in PostgreSQL with Redis-based distributed locking.Nconfigc                 8    |xs
 t               | _        d | _        y r#   )r   r?   _conn)r'   r?   s     r(   __init__zTokenVault.__init__D   s    0 0
r*   c                 z   | j                   | j                   j                  rTt        j                         }|j	                  t
               t        j                  di || _         d| j                   _        	 | j                   j                         5 }|j                  d       ddd       | j                   S # 1 sw Y   | j                   S xY w# t        $ rb t        j                         }|j	                  t
               t        j                  di || _         d| j                   _        Y | j                   S w xY w)z,Get a PostgreSQL connection with keepalives.NTzSELECT 1r<   )rA   closedr
   get_connection_paramsupdatePG_KEEPALIVE_PARAMSpsycopg2connect
autocommitcursorexecute	Exception)r'   paramscurs      r(   	_get_connzTokenVault._get_connH   s    ::!2!2#99;FMM-.!))3F3DJ$(DJJ!	)""$ (J'( zz( zz  	)#99;FMM-.!))3F3DJ$(DJJ!zz	)s1   8C B8$C 8C=C C AD:9D:c                 R    ddl } |j                  di t        j                         S )z/Get a Redis connection for distributed locking.r   Nr<   )redisRedisr   rE   )r'   rR   s     r(   
_get_rediszTokenVault._get_redisZ   s"    u{{A[>>@AAr*   c                    d}t        |      5 }|j                         }ddd       | j                         }|j                         5 }|j	                         ddd       t
        j                  d       y# 1 sw Y   YxY w# 1 sw Y   +xY w)z"Create tables if they don't exist.z-/mnt/e/genesis-system/GHL/oauth/db_schema.sqlNzGHL OAuth schema initialized)openreadrP   rK   rL   loggerinfo)r'   schema_pathfsqlconnrO   s         r(   init_schemazTokenVault.init_schema_   sv    E+ 	!&&(C	~~[[] 	cKK	23	 		 	s   A7B7B Br   r   
expires_inr   r!   c                    t        j                  t        j                        t	        |      z   }| j                         }|j                         5 }|j                  d||||f       t        |j                         d         }|j                  d|t        j                  j                  d|j                         t        |      d      f       ddd       t        j!                  d|j                                 S # 1 sw Y   1xY w)	zAStore the agency-level OAuth token from initial app installation.r,   aq  
                INSERT INTO ghl_oauth_tokens
                    (token_type, location_id, access_token, refresh_token, scopes, expires_at)
                VALUES ('agency', NULL, %s, %s, %s, %s)
                ON CONFLICT (token_type, location_id)
                DO UPDATE SET
                    access_token = EXCLUDED.access_token,
                    refresh_token = EXCLUDED.refresh_token,
                    scopes = EXCLUDED.scopes,
                    expires_at = EXCLUDED.expires_at,
                    last_refreshed_at = NOW(),
                    revoked_at = NULL
                RETURNING id
            r   z
                INSERT INTO ghl_oauth_audit (token_id, operation, details)
                VALUES (%s, 'created', %s)
            agency)typer   scope_countNzAgency token stored, expires )r   r$   r   r%   r   rP   rK   rL   r7   fetchonerH   extrasJson	isoformatlenrX   rY   )	r'   r   r   r_   r   r   r]   rO   token_ids	            r(   store_agency_tokenzTokenVault.store_agency_tokeni   s     \\(,,/)J2OO
~~[[] 	cKK  vzBD 3<<>!,-H KK  HOO00 (224"6{2  '	8 	3J4H4H4J3KLM;	 	s   A?D  D	r   r   c                    t        j                  t        j                        t	        |      z   }| j                         }|j                         5 }|j                  d|||||f       t        |j                         d         }	|j                  d|	|t        j                  j                  ||j                         d      f       ddd       t        j                  d| d| d	       	S # 1 sw Y   'xY w)
z>Store a per-location token from /oauth/locationToken exchange.r,   aq  
                INSERT INTO ghl_oauth_tokens
                    (token_type, location_id, location_name, access_token, scopes, expires_at)
                VALUES ('location', %s, %s, %s, %s, %s)
                ON CONFLICT (token_type, location_id)
                DO UPDATE SET
                    access_token = EXCLUDED.access_token,
                    location_name = EXCLUDED.location_name,
                    scopes = EXCLUDED.scopes,
                    expires_at = EXCLUDED.expires_at,
                    last_refreshed_at = NOW(),
                    revoked_at = NULL
                RETURNING id
            r   z
                INSERT INTO ghl_oauth_audit (token_id, operation, location_id, details)
                VALUES (%s, 'exchanged', %s, %s)
            )r   r   NzLocation token stored for  ())r   r$   r   r%   r   rP   rK   rL   r7   rd   rH   re   rf   rg   rX   rY   )
r'   r   r   r   r_   r   r   r]   rO   ri   s
             r(   store_location_tokenzTokenVault.store_location_token   s     \\(,,/)J2OO
~~[[] 	cKK  }lFJOQ 3<<>!,-HKK  K)=)=!.(224? * #	2 	0RaPQ5	 	s   A7C..C7c                    | j                         }|j                  t        j                  j                        5 }|j                  d       |j                         }|s
	 ddd       y| j                  |      cddd       S # 1 sw Y   yxY w)zGet the current agency token.cursor_factoryz
                SELECT * FROM ghl_oauth_tokens
                WHERE token_type = 'agency' AND revoked_at IS NULL
                ORDER BY created_at DESC LIMIT 1
            NrP   rK   rH   re   
DictCursorrL   rd   _row_to_recordr'   r]   rO   rows       r(   get_agency_tokenzTokenVault.get_agency_token   s{    ~~[[(B(B[C 		,sKK  
 ,,.C		, 		, &&s+		, 		, 		,s   %B)BBc                 "   | j                         }|j                  t        j                  j                        5 }|j                  d|f       |j                         }|s
	 ddd       y| j                  |      cddd       S # 1 sw Y   yxY w)z4Get the token for a specific location (sub-account).rp   z
                SELECT * FROM ghl_oauth_tokens
                WHERE token_type = 'location'
                  AND location_id = %s
                  AND revoked_at IS NULL
                ORDER BY created_at DESC LIMIT 1
            Nrr   )r'   r   r]   rO   rv   s        r(   get_location_tokenzTokenVault.get_location_token   s    ~~[[(B(B[C 	,sKK  ! ,,.C	, 	, &&s+	, 	, 	,s   'B+BBc                 (   | j                         }|j                  t        j                  j                        5 }|j                  d       |j                         D cg c]  }| j                  |       c}cddd       S c c}w # 1 sw Y   yxY w)zGet all active location tokens.rp   z
                SELECT * FROM ghl_oauth_tokens
                WHERE token_type = 'location' AND revoked_at IS NULL
                ORDER BY location_name
            N)rP   rK   rH   re   rs   rL   fetchallrt   ru   s       r(   get_all_location_tokensz"TokenVault.get_all_location_tokens   s    ~~[[(B(B[C 	HsKK  
 9<GD'',G	H 	H H	H 	Hs   $BB7BBBc           	         | j                         }d}|j                  |ddd      s%t        j                  d       | j	                         S 	 | j	                         }|r|j
                  s(t        j                  d       	 |j                  |       y|j                  s||j                  |       S t        j                  | j                  j                  | j                  j                  | j                  j                  d	|j
                  d
ddid      }|j                  dk7  rbt        j                  d|j                   d|j                           | j#                  dd|j                   dd       	 |j                  |       y|j%                         }|j'                  d	|j
                        }| j)                  |d   ||j'                  d| j                  j*                        |j'                  d      r!|j'                  dd      j-                  d      n|j.                         t        j                  d       | j	                         |j                  |       S # |j                  |       w xY w)zSRefresh the agency token using the refresh_token. Uses Redis lock to prevent races.zghl_oauth:refresh_lock:agency1Tr   )nxexz8Another process is refreshing the agency token, skippingz*No agency token or refresh_token availableNr   )	client_idclient_secret
grant_typer   Content-Typez!application/x-www-form-urlencoded)dataheaderstimeout   zAgency token refresh failed:  failedrefresh_agencyerror	operationr   r_   scope )r   r   r_   r   z#Agency token refreshed successfully)rT   setrX   rY   rw   r   r   deleter0   requestspostr?   	token_urlr   r   status_codetext_auditjsongetrj   access_token_ttl_secondssplitr   )r'   rlock_keyra   respr   new_refreshs          r(   refresh_agency_tokenzTokenVault.refresh_agency_token   s   OO2 uuXstu3KKRS((**(	**,F!5!5IJH HHXC ''@ HHX= ==%%!%!6!6%)[[%>%>"1%+%9%9	 ()LM
D 3&<T=M=M<NaPTPYPY{[\HddiiN^,_` HHX 99;D((?F4H4HIK##!.1)88L$++2V2VW;?88G;Ltxx,2237RXR_R_	 $  KK=>((* HHXAHHXs    3I$ I$ 3CI$ 
CI$ $I7c           
      ~   | j                         }|st        j                  d       y|j                  r| j	                         }|syt        j                  | j                  j                  | j                  j                  |dd|j                   | j                  j                  ddd      }|j                  d	k7  rSt        j                  d
| d|j                   d|j                          | j                  d||j                  dd       y|j                         }| j!                  |||d   |j#                  d| j                  j$                        |j#                  d      r!|j#                  dd      j'                  d      ng        t        j)                  d| d| d       | j+                  |      S )z;Exchange agency token for a location-specific access token.z/No agency token available for location exchangeN)	companyId
locationIdzBearer zapplication/json)AuthorizationVersionr   r   )r   r   r   r   z#Location token exchange failed for z: r   r   exchange_locationr   r   r_   r   r   )r   r   r   r_   r   zLocation token exchanged for rl   rm   )rw   rX   r   r0   r   r   r   r?   location_token_url
company_idr   api_versionr   r   r   r   rn   r   r   r   rY   ry   )r'   r   r   ra   r   r   s         r(   exchange_location_tokenz"TokenVault.exchange_location_token  s   &&(LLJK ..0F}}KK**![[33)
 $+6+>+>*?!@;;22 2
 
 s"LL>{m2dN^N^M__`aeajaj`klmKK+Qd/efyy{!!#'n-xxdkk.R.RS7;xx7H488GR(..s3b 	" 	
 	3K==/QRST&&{33r*   c                 V   | j                  |      }|r?|j                  r3|j                  s'| j                  |j                         |j
                  S |r|j                  nd}| j                  ||      }|r3|j                  r'| j                  |j                         |j
                  S y)zBGet a valid access token for a location. Auto-refreshes if needed.r   N)ry   r2   r0   
_mark_usedr   r   r   r   )r'   r   tokenname	new_tokens        r(   get_valid_tokenzTokenVault.get_valid_tokenB  s    ''4U^^E,?,?OOEHH%%%% ',u""00dC	++OOILL))))r*   	locationsc                     i }|D ]d  }|j                  d      xs |j                  d      }|j                  d      xs |j                  dd      }| j                  ||      }|du||<   f |S )zBExchange tokens for all locations. Returns {location_id: success}.r   r   r   r   r   N)r   r   )r'   r   resultslocloc_idloc_namer   s          r(   exchange_all_locationsz!TokenVault.exchange_all_locationsT  su     	0CWWT]<cggm&<FwwvF#''/2*FH00BE#4/GFO		0
 r*   c                    | j                         }|j                  t        j                  j                        5 }|j                  d       g }|j                         D ]V  }|j                  |d   |d   |d   |d   r|d   j                         nd|d   r|d   j                         nd|d	   d
       X |t        |      dcddd       S # 1 sw Y   yxY w)z%Get the current status of all tokens.rp   a  
                SELECT token_type, location_id, location_name,
                       expires_at, last_used_at, revoked_at,
                       CASE WHEN expires_at > NOW() THEN 'valid'
                            ELSE 'expired' END as status
                FROM ghl_oauth_tokens
                WHERE revoked_at IS NULL
                ORDER BY token_type, location_name
            r   r   r   r   Nr   status)rb   r   r   r   	last_usedr   )tokenstotal)
rP   rK   rH   re   rs   rL   r{   appendrg   rh   )r'   r]   rO   r   rv   s        r(   r   zTokenVault.status^  s    ~~[[(B(B[C 	<sKK   F||~ -#&}#5%(%9CF|CT#l"3"="="?Z^DGDW^!4!>!>!@]a!(m  %s6{;)	< 	< 	<s   B
CCri   c                     | j                         }|j                         5 }|j                  d|f       ddd       y# 1 sw Y   yxY w)zUpdate last_used_at timestamp.z>UPDATE ghl_oauth_tokens SET last_used_at = NOW() WHERE id = %sN)rP   rK   rL   )r'   ri   r]   rO   s       r(   r   zTokenVault._mark_usedw  sB    ~~[[] 	cKKP	 	 	s	   >Ar   detailsc           	          | j                         }|j                         5 }|j                  d||t        j                  j                  |      f       ddd       y# 1 sw Y   yxY w)zWrite an audit log entry.z
                INSERT INTO ghl_oauth_audit (operation, location_id, details)
                VALUES (%s, %s, %s)
            N)rP   rK   rL   rH   re   rf   )r'   r   r   r   r]   rO   s         r(   r   zTokenVault._audit  s_    ~~[[] 	JcKK  [(//*>*>w*GHJ	J 	J 	Js   3AA&c                     t        t        | d         | d   | d   | d   | d   | d   | d   xs g | d   | d	   | d
   
      S )Nr   r   r   r   r   r   r   r   r   r    )
r   r   r   r   r   r   r   r   r   r    )r   r7   )rv   s    r(   rt   zTokenVault._row_to_record  sf    3t9~<(M*o.^,o.x=&B<(^,<(
 	
r*   r#   )r   ) r3   r4   r5   r6   r   r   rB   rP   rT   r^   r7   intr9   rj   rn   r   rw   ry   r|   r   r   r   r   r;   r   r   r   r   dictr   staticmethodrt   r<   r*   r(   r>   r>   A   s   Vx7 $B
4'' ' 	'
 ' 
'R%% % 	%
 % % 
%N,(;"7 ,,c ,h{6K , 	H 	H2h{&; 2h*43 *4s *4T\]hTi *4X3 8C= $ c4i <S#X <23 J J(3- J$ J 
{ 
 
r*   r>   )r6   sysloggingr   r   r   typingr   r   r   dataclassesr	   rH   psycopg2.extrasr   pathr   elestio_configr
   r   GHL.oauth.configr   	getLoggerr3   rX   rG   r   r>   r<   r*   r(   <module>r      s   	   2 2 & & !    ; < 6 +			8	$ 	  ? ? ?6U
 U
r*   