
    |;i                        U d Z ddlZddlZddlZddlZddlZddlZddlmZmZ ddl	m
Z
mZmZ ddlmZ ddlmZmZmZmZmZmZmZ ddlmZmZmZ ddlmZ dd	lmZ dd
lm Z  ddl!m"Z"m#Z# ddl$m%Z% ddl&m'Z' ddl(m)Z)m*Z* ddl&m+Z+ ddl,m-Z- ddl.Z.ddlm/Z/ ddl0m1Z1m2Z2m3Z3m4Z4m5Z5m6Z6 ddl7m8Z9 ddl:m;Z;  e;         e       Z<ddl=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZDmEZEmFZFmGZGmHZH  ej                  ej                          ej                  eL      ZM ej                  dd      ZO ej                  eOd        ej                  dd      ZQeO deQ ZRdZSdeTdeUfd ZVdgd!eUfd"ZW eXeRd#      5 ZY e)j                  eYj                         d e-       $      Z\ddd       e\j                         j                         Z_d%d&d'eS eVe_j                         eVe_j                        d(Zbi Zci Zde
eUeUf   eed)<   deTfd*Zfd+eUd,eUd!eUdeUfd-Zgd.eUdee5   fd/Zhd0eUdeUfd1Zid2eUfd3Zje<j                  d4      d5        Zle<j                  d6      d7        Zme<j                  d8d9d:g;      d<eUfd=       Zod>eUdefd?Zpe<j                  d@d9d:g;      dAefdB       Zq G dC dDe       Zre<j                  dE      dAefdF       Zte<j                  dG       edH      fdIeUfdJ       Zue<j                  dK      d<eUfdL       Zve<j                  dM      d<eUfdN       Zwe<j                  dO       edP       edQ      fd<eUdReUdSexfdT       Zye<j                  dU       edH       edH       edV       edQ      fd<eUdWeUdXeUdYexdSexf
dZ       Zze<j                  d[      d<eUfd\       Z{e<j                  d]       edH       ed^       ed_       edQ      fd<eUd`eUdaeUdSexfdb       Z|e<j                  dc      dd        Z}e<j                  de      df        Z~y# 1 sw Y   ixY w)hz
FastAPI LTI 1.3 Tool example
- OIDC login -> LTI launch (id_token verification)
- Token exchange (client_assertion JWT)
- Calls to Names & Roles / AGS / Deep Linking
- JWKS hosting for the Tool (demo)
    N)datetime	timedelta)DictAnyOptional)	urlencode)FastAPI	APIRouterDependsRequestResponseFormHTTPException)RedirectResponseJSONResponseHTMLResponse)StaticFiles)URL)	BaseModel)jwtjwk)base64url_decode)rsa)serializationhashes)padding)default_backend)urlparse)LMSPlatform
DeploymentUserUserCourseLogAccessToken
LoginState)load_dotenv)TOOL_CLIENT_IDTOOL_REDIRECT_URITOOL_BASE_URLTOOL_JWKS_PATHTOOL_JWKS_URLPLATFORM_ISSPLATFORM_OIDC_AUTHPLATFORM_TOKEN_URLPLATFORM_JWKS_URLDEPLOYMENT_IDSCOPES)levelTOOL_KEY_DIRz./keysT)exist_okTOOL_PRIVATE_KEY_PEMzprivate.pem/
tool-key-1valreturnc                     | j                  | j                         dz   dz  d      }t        j                  |      j	                  d      j                  d      S )z1Convert integer to base64url string (no padding).      big)	byteorder   =zutf-8)to_bytes
bit_lengthbase64urlsafe_b64encoderstripdecode)r7   	int_bytess     ./var/www/html/content_weaver/routers/canvas.pyb64url_uintrG   H   sL    cnn.2q8EJI##I.55d;BB7KK    kidc           	         t        j                  ddt                     }t        t        d      5 }|j                  |j                  t        j                  j                  t        j                  j                  t        j                                      d d d        t        d       |j                         }|j                         }ddd	t         t#        |j$                        t#        |j&                        d
}t        t(         dd      5 }t+        j,                  ||d       d d d        t        d       |S # 1 sw Y   xY w# 1 sw Y   "xY w)Ni  i   )public_exponentkey_sizebackendwbencodingformatencryption_algorithmu$   ✅ Saved private key to private.pemRSARS256sigktyalguserI   nez/public_jwk.jsonw   )indentu'   ✅ Saved public JWK to public_jwk.json)r   generate_private_keyr   openPRIVATE_KEY_PATHwriteprivate_bytesr   EncodingPEMPrivateFormatPKCS8NoEncryptionprint
public_keypublic_numbersPRIVATE_KEY_KIDrG   rZ   r[   r2   jsondump)rI   private_keyfrj   numbersr   s         rF   generate_rsa_keypairrr   M   s*   **!K 
	% 
	%%&//33$2288%2%?%?%A & 	

 

01 '')J'')G ##C 
./	5 $		#q#$	
34J9
 
0$ $s   A&D>E
>E
ErbpasswordrM   rS   rT   rU   rV   NONCE_STOREc                  <    t        t        j                               S N)inttime rH   rF   nowr|      s    tyy{rH   	client_id	token_urlc           	         t        t        j                         j                               }|dz   }| | |||t	        t        j                               d}|j                  t        j                  j                  t        j                  j                  t        j                               }d|d}t        j                  d|       t        j                  d|       t!        j"                  ||d|      S )	zO
    Create a signed JWT client assertion for Canvas OAuth2 token request.
    i,  )isssubaudiatexpjtirO   rT   )rX   rI   zClient assertion headers: %szClient assertion payload: %s	algorithmheaders)ry   r   utcnow	timestampstruuiduuid4rc   r   rd   re   rf   rg   rh   loggerinfor   encode)	r}   r~   ro   rI   r   r   payloadpemr   s	            rF   create_client_assertionr      s     hoo))+
,C
)C 4::< G 
#
#''++**00*779 $ C c*G
KK.8
KK.8 ::gsgwGGrH   emailc                    | j                  t              j                  t        j                  |k(        j	                         }| j                  t
              j                  t
        j                  |j                  k(  t        j                  |k(        j                  t
        j                  j                               j	                         }|r.|j                  t        j                         kD  r|j                  S y y rx   )queryr!   filterr   firstr#   user_ididorder_by
expires_atdescr   r   token)dbr   userr   s       rF   get_latest_tokenr      s    88D>  u!45;;=D
$$477*JJ%	
 (;))..0
1%%'	 
 !!HOO$55{{ 6urH   scopec                    t        j                         }| j                  t              j	                  t        j
                  |j                  k(  t        j                  |j                  k(  t        j                  |j                  k(  t        j                  |k(  t        j                  |kD        j                  t        j                  j                               j                         }|r|j                  S t        |j                   |j"                  t$        t&              }t(        j+                  d|       dd||d}t(        j+                  d|       t-        j.                  |j"                  |      }	t(        j+                  d|	j0                         |	j3                          |	j5                         }
|
d	   }|
j7                  d
d      }|t9        |      z   }t        |j                  |j                  |j                  |||      }| j;                  |       | j=                          | j?                  |       |j                  S )z
    Return a valid access_token for the given platform/deployment/user/scope.
    If expired or missing, request a new one from Canvas and store it.
    )r}   r~   ro   rI   zclient_assertion: %sclient_credentialsz6urn:ietf:params:oauth:client-assertion-type:jwt-bearer)
grant_typeclient_assertion_typeclient_assertionr   zToken request payload: %s)datazToken response status: %saccess_token
expires_ini  )seconds)platform_iddeployment_idr   r   r   r   ) r   r   r   r#   r   r   r   r   r   r   r   r   r   r   r   r   r}   r~   PRIVATE_KEYrl   r   r   httpxposttextraise_for_statusrm   getr   addcommitrefresh)r   platform
deploymentr   r   r|   r   r   r   response
token_jsonr   r   r   	new_tokens                  rF   get_valid_tokenr      s   
 //
C 		##x{{2%%6477*&""S(

 
+((--/	0	 
 {{ /$$$$	 KK&(89*!Y,	G KK+W5zz(,,7;H
KK+X]];Jn-Ld3Jy44J KK mmI FF9IIKJJy??rH   tenant_domainc                    |d   }|d   }| d}| d}|j                  di       }|j                  dd      }|j                  dd      }	| j                  t              j                  ||	      j	                         }
|
sEt        |||	||||
      }
| j                  |
       | j                          | j                  |
       n3||
_        |	|
_	        ||
_
        ||
_        ||
_        | j                          |d   }| j                  t              j                  |
j                  |      j	                         }|sIt        |
j                  |      }| j                  |       | j                          | j                  |       |d   }|j                  dg       }d|v rd}nd}| j                  t              j                  |j                  d            j	                         }|st        |j                  d      |j                  d      |j                  d      |j                  d      |
j                  |t!        j"                  |      d|dddt%        j&                         t%        j&                               }| j                  |       | j                          | j                  |       |dk(  rDt)        |j                  d|
j                  d      }| j                  |       | j                          |
||fS )Nr   r   /api/lti/security/jwksz/login/oauth2/token7https://purl.imsglobal.org/spec/lti/claim/tool_platformname guidissuerr}   r   r   	tenant_idtenant_namer}   jwks_urlr~   7https://purl.imsglobal.org/spec/lti/claim/deployment_id)r   r   r   /https://purl.imsglobal.org/spec/lti/claim/rolesGhttp://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator   r]   r   r   
given_namefamily_name   activer   )r   
first_name	last_namer   r   r   	lti_roles	is_activerole_idsubscription_statusis_email_verifiedis_phone_verified
created_at
updated_at
   Credit)r   number_of_courser   type)r   r   r   	filter_byr   r   r   r   r   r   r   r   r~   r    r   r!   rm   dumpsr   r   r"   )r   decoded_tokenr   r   r}   r   r~   tool_plateformplatform_namer   r   r   r   r   rolesr   r   UserCourseLogEntrys                     rF   store_launchr     s   5!Fe$I   67H /!45I #&&'`bdeN"&&vr2M $$VR0K xx$.. /  eg  '!%
 	x
		


8 "/&*$&
		 ""[\M*%//KK# 0  eg  HKK}U

z
		


:

COQSTEPTYY88D>##-*;*;G*D#EKKMD""6*$((6#''6##G, 

5) (((
  	t
		


4 a<!.!#$KK	" FF%&IIKZ%%rH   z
/jwks.jsonc                  .   K   t        dt        gi      S w)zcHost the tool's JWKS containing the public key (so platform can verify client_assertion if needed).keys)r   TOOL_JWKr{   rH   rF   jwksr   k  s      (,--s   z/demo/launchc                     K   d} d}d}t        t        j                               }| |||t        t        j                               d}dt        |       }t	        t        |            S w)z
    Simulate Canvas launching your tool (for local testing only).
    Builds an OIDC login initiation request and redirects to /oidc/login.
    zhttps://canvas.instructure.comzdemo-login-hintz#http://localhost:8000/canvas/launch)r   
login_hinttarget_link_urilti_message_hintstatez/canvas/oidc/login?)r   r   r   r   r   )r   r   r   r   paramsurls         rF   demo_launchr   p  sn      +C"J;O4::<(  *,TZZ\"F  	& 12
3CCH%%s   A)A+z/launchGETPOST)methods	launch_idc                   K   t        t        j                               }t        j	                  |       }|st        dd      |d   }|j	                  d      }|j	                  dg       }t        j                  d|       t        j                  d|       |j	                  d	      }|j	                  d      t        d
d      t        |||      \  }}}	t        d   }
|j	                  d      }	 t        ||||	|
      }i }|j	                  d      j	                  d      }|j	                  d      }|j	                  d      }t        j                   dd      }|j	                  di       }|j	                  dd      }|j	                  dd      }|j	                  di       j	                  dd      }|j	                  di       j	                  dd      }|j	                  di       }|j	                  dd      }|j	                  dd      }|j	                  dd      }dj#                  |      }dj#                  g d | d!|j$                   d"| d#|j&                   d$|  d%| d&|	j(                   d'| d(| d)|j	                  d*       d+|j	                  d,       d-| d.| d/| d0| d1| d2|j*                   d3| d4| d5| d6| d7| d8| d9| d:| d;| d<      }t-        |      S # t        $ r`}t        ddt        |       d|
 dt        |j                        t        |j                        t        |	j                        f       d }~ww xY ww)=N  zUnknown launch IDstatus_codedetaildecodedr   r   zSub Data: %szLaunch decoded token: %sr     zMissing sub claim in id_tokennrpsr     zFailed to get access token: z, z0https://purl.imsglobal.org/spec/lti/claim/lti1p1r   r   r   FRONT_APP_URLz%https://answerous.contactous.com:9001z7https://purl.imsglobal.org/spec/lti/claim/resource_linkr   r   titlez1https://purl.imsglobal.org/spec/lti/claim/contextr   r   product_family_code,z)
    <html>
    <body>
    <form action="zb/api/canvas" method="POST" style="display: none;">
        <input type="hidden" name="iss" value="z3"/>
        <input type="hidden" name="sub" value="z9"/>
        <input type="hidden" name="client_id" value="z9"/>
        <input type="hidden" name="launch_id" value="z7"/>
        <input type="hidden" name="user_id" value="z;"/>
        <input type="hidden" name="lms_user_id" value="zD"/>
        <input type="hidden" name="lis_person_name_full" value="zP"/>
        <input type="hidden" name="lis_person_contact_email_primary" value="zE"/>
        <input type="hidden" name="lis_person_name_given" value="r   zF"/>
        <input type="hidden" name="lis_person_name_family" value="r   z<"/>
        <input type="hidden" name="custom_email" value="zK"/>
        <input type="hidden" name="custom_canvas_user_login_id" value='z<'/>
        <input type="hidden" name="access_token" value="z="/>
        <input type="hidden" name="tenant_domain" value="zH"/>
        <input type="hidden" name="custom_canvas_api_domain" value="zA"/>
        <input type="hidden" name="lti_deployment_id" value="z?"/>
        <input type="hidden" name="target_link_uri" value="z:"/>
        <input type="hidden" name="context_id" value="z="/>
        <input type="hidden" name="context_title" value="z@"/>
        <input type="hidden" name="resource_link_id" value="zC"/>
        <input type="hidden" name="resource_link_title" value="zK"/>
        <input type="hidden" name="tool_consumer_instance_guid" value="zK"/>
        <input type="hidden" name="tool_consumer_instance_name" value="zZ"/>
        <input type="hidden" name="tool_consumer_instance_product_family_code" value="zw"/>
        <input type="hidden" name="lti_version" value='LTI-1p3'/>
        <input type="hidden" name="roles" value='z'/>
        <input type="submit" value="Start Deep Linking Flow"/>
    </form>
    <script type="text/javascript">
        document.forms[0].submit();
    </script>
  </body>
</html>
    )nextdbaseget_dbREGISTRATIONSr   r   r   r   r   r0   r   	Exceptionr   dict__dict__osgetenvjoinr   r}   r   r   r   )r   r   regr   user_subr   r   r   r   r   r   r   r[   roster_datalti_user_idr   r   FRONTEND_APPresource_link_claimresource_idresource_title
context_idcontext_titler   r   r   r  htmls                               rF   launch_pager    s    	elln	B


I
&C4GHH)nG{{5!HKKI2NE
KK)
KK*G4GGO,M{{5!4STT!-b'=!IHj$6NE77>*Lq&r8ZuM K++PQUUV_`KKK E;;vD99_.UVL!++&_acd%))$3K(,,Wb9NPRTUYYZ^`bcJKK SUWX\\]dfhiM[[!Z\^_N"&&vr2M $$VR0K(,,-BBG HHUOE% %  %  .%!0% 19/@	%A0	%
 19z%
:6% 7?6H6H5I%J6% 7@[%A4% 5@=%A8% 9=y%AA% BF%GM% NSG%TB% CJ++lB[A\%]C% DK;;}C]B^%_9% :?%@H% INw%O9%  :F!% G:!%" ;H#%"IE#%$ FSO%%$T>%%& ?I>V>V=W'%&X<'%( =I>)%(J7)%* 8Bl+%*C:+%, ;H-%,I=-%. >IM/%.J@/%0 AO?O1%0PH1%2 IT}3%2UH3%4 IV5%4WW5%6 XkVk7%6l27%: 38;%:9;%DL {  q6RSVWXSYRZZ\]b\ccefjksk|k|f}  @D  EO  EX  EX  @Y  [_  `d  `m  `m  [n  gn  fo  5p  q  	qqs,   C0N3L HN	M>AM99M>>Nr   c                    | j                  d      }|st        dd      |d   }t        d   }	 t        j	                  d|       dd	| i}t        j                   ||
      }|j                          |j                         S # t        $ r6}t        j                  dt        |             dt        |      icY d }~S d }~ww xY w)N?https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservicer   No NRPS endpoint in id_tokenr   context_memberships_urlr   zUsing access token: %sAuthorizationBearer )r   zFailed to fetch roster: %serror)r   r   r0   r   r   r   r   rm   r
  r"  r   )r   r   
nrps_claimcontext_urlr   r   respr[   s           rF   fetch_rosterr&    s    ^_J4RSS67K6NE	!,l;"gl^$<=yyg6yy{ !13q6:Q  !s   AB 	C+B>8C>Cz/oidc/loginrequestc                 `  K   t        t        j                               }t        | j                        }| j
                  dk(  r)| j                          d{   }|j                  |       |j                  d      }|j                  d      }|j                  d      }|j                  d      }| j                  j                  d      xs | j                  j                  d      }d}	|r&t        |      }
|
j                   d	|
j                   }	|	s0|r.	 t        j                  |      }|j                  d
      }|rd| }	t"        j%                  d|	       t'        t)        j*                               }t-        ||	      }|j/                  |       |j1                          t'        t)        j*                               }dt2        |<   ddt4        t6        d||||dd
}t8         dt;        |       }t=        t'        |            S 7 # t        $ r}t!        d|       Y d}~d}~ww xY ww)zq
    OIDC login initiation endpoint.
    Canvas may call this with POST (hidden form) or GET (query params).
    r   Nr   r   r   r   refereroriginz://canvas_domainzhttps://z"Failed to decode lti_message_hint:zDetected tenant domain: %s)r   r   pendingid_token	form_postopenidnone)
response_typeresponse_moder}   redirect_urir   r   r   noncer   prompt?)r  r  r  r  query_paramsmethodformupdater   r   r   schemenetlocr   get_unverified_claimsr
  ri   r   r   r   r   r   r$   r   r   rv   r&   r'   r,   r   r   )r'  r   r   r9  r   r   r   r   r)  r   parseddecoded_hintr+  r[   r   login_stater4  redirect_paramsr   s                      rF   
oidc_loginrB    s     
elln	B'&&'F ~~\\^#d
**U
CL)Jjj!23Ozz"45 oo!!),M0C0CH0MGM'"!==/V]]O< -	;445EFL(,,_=M"*=/ : KK,m< 

E5FKFF;IIK

E"K $$#) ,O   )O"<!=
>CCH%%m $,  	;6::	;s>   AH.H	B?H.-H CH.	H+H&!H.&H++H.c                   ,    e Zd ZU eed<   dZee   ed<   y)OIDCLaunchFormr-  Nr4  )__name__
__module____qualname__r   __annotations__r4  r   r{   rH   rF   rD  rD  ?  s    ME8C=rH   rD  z/oidc/callbackc                   K   t        t        j                               }| j                          d{   }| j	                          d{   }|j                  d      }|j                  d      }|sdt        |      |j                         dS |j                  t              j                  |      j                         }|st        dd	      |j                  }|st        dd
	      t        j                  d|       t!        j"                         4 d{   }|j                  | d       d{   }	|	j%                         }
ddd      d{    t'        j(                  |      }|j                  d      t        fd
d   D        d      }|st        dd	      	 t'        j                  ||dgt*        t,              }|j                  d      }t1        t3        j4                               }|||t7        t9        j8                               dt:        |<   |j=                  |       |j?                          |j                  dt@         d      }| d| }tC        |      S 7 >7 )7 [7 B7 %# 1 d{  7  sw Y   6xY w# t.        $ r}t        ddt1        |       	      d}~ww xY ww)z
    OIDC callback endpoint.
    Canvas POSTs here with id_token + state.
    After verifying, redirect to target_link_uri (the launch page).
    Nr-  r   zCanvas did not send id_token)r"  r9  raw)r   r   zInvalid or expired stater   zMissing tenant domain for statez"Using tenant domain from state: %sr   rI   c              3   4   K   | ]  }|d    k(  s|  yw)rI   Nr{   ).0krI   s     rF   	<genexpr>z oidc_callback.<locals>.<genexpr>h  s     ;a1U8s?;s   r   zNo matching JWKS keyrT   )
algorithmsaudiencer   zInvalid id_token: r   )r   r   r   	issued_atz9https://purl.imsglobal.org/spec/lti/claim/target_link_uriz/canvas/launchz?launch_id=)"r  r  r  bodyr9  r   r  rD   r   r$   r   r   r   r   r   r   r   AsyncClientrm   r   get_unverified_headerr&   r+   r
  r   r   r   ry   rz   r	  deleter   r(   r   )r'  r   rJ  r9  r-  r   r@  r   clientrr   headerkeyr   r[   r   r   r   redirect_urlrI   s                      @rF   oidc_callbackr[  C  st     
elln	B
CDxx
#HHHWE7dTWT^T^T`aa ((:&00u0=CCEK4NOO--M4UVV
KK4mD   "  f**.DEFFvvx 
 &&x0F
**U
C
;4<;T
BC4JKK
S**y#
 KK YZM
 DJJL!I &%	 M) IIkIIK kkC/(O &&k)=LL))O &F   *  S6HQ4QRRSs   1KI2KI5CKI8K!J9I;:JKI>AK,#J B$K5K8K;J>KJJ
JK	J> J99J>>Kz/lti/canvasdata.canvas_datac                   K   t        t        j                               }| st        dddi      S t	        j
                  |       }t        j                  d|       i }|j                  d      rd|j                  d       }t        j                  d|j                  d             |j                  t              j                  |j                  d      	      j                         }|r=t        j                  d
t        |j                               t        j                  d|j                         |j                   s|j                  t"              j                  |j                  d      r|j                  d      nt$        |j                  d      r|j                  d      n|      j                         }|st#        |j                  d      r|j                  d      nt$        |j                  d      |j                  d      |j                  d      |j                  d      r|j                  d      n|dd      }|j'                  |       |j)                          |j+                  |       nR|j                  d      |_        |j                  d      |_        |j                  d      |_        |j)                          |j2                  |_        |j)                          |j2                  |j4                  |j                  |j                   |j6                  d}n>t        j9                  d       |j                  t"              j                  |j                  d      r|j                  d      nt$        |j                  d      r|j                  d      n|      j                         }|st#        |j                  d      r|j                  d      nt$        |j                  d      |j                  d      |j                  d      |j                  d      r|j                  d      n|dd      }|j'                  |       |j)                          |j+                  |       nR|j                  d      |_        |j                  d      |_        |j                  d      |_        |j)                          |j                  dd      }t;        |t<              r>|j?                  d      D cg c]#  }|jA                         s|jA                         % }	}nXt;        |tB        tD        f      r@|D cg c]4  }t;        |t<              s|jA                         s%|jA                         6 }	}ng }	|j                  d      dk(  sd|j                  d      v sd|	v rd}
nd}
t        |j                  dd      |j                  dd      |j                  d d      |j                  d      |j2                  |j                  d!t=        tG        jH                                     t	        jJ                  |	      d"|
d#d$d$tM        jN                         tM        jN                         tM        jN                         %      }|j'                  |       |j)                          |j+                  |       |
dk(  rDtQ        |j2                  |j2                  d&d'(      }|j'                  |       |j)                          |j2                  |j4                  |j                  |j                   |j6                  d}t        j                  d)       nt        j9                  d*       t        d+|d,      S c c}w c c}w w)-zv
    Canvas Data Webhook endpoint.
    Canvas can POST data exports here.
    canvas_data is a large JSON string.
    r   r"  zMissing canvas_data)r   contentz Received Canvas Data webhook: %scustom_canvas_user_login_idzLTI-1p0-client-zData for user: %sr   zUser Data: %szFound user in DB: %sr   r}   r   custom_canvas_api_domaintool_consumer_instance_guidtool_consumer_instance_namer   r   )r   r   r   r   r   zUser not found in DB	ext_rolesr  r   z&urn:lti:instrole:ims/lis/Administratorr   r   r]   lis_person_name_fullUnknownlis_person_name_givenlis_person_name_familyr   r   r   r   )r   r   r   r   r   r   r   r   r   r   r   r   activation_dater   r   r   r   )r   r   r   r   zCreated new user in DBzNo user login ID in payloadreceived)statusr   ))r  r  r  r   rm   loadsr   r   r   r   r!   r   r   r  r  r   r   r   r+   r   r   r   r   r   r   r   r   r   warning
isinstancer   splitstriplisttupler   r   r   r   r   r"   )r\  r   r   	user_datacustom_client_idr   r   ext_roles_rawrW  rc  r   r   s               rF   canvas_data_webhookru    s     
elln	B34
 	
 jj%G
KK2G<I{{01,W[[9V-W,XY'5R)STxx~''gkk:W.X'Y__aKKdmm)<=KK.

;##88K0::18U1C7;;u-:A++k:Rgkk+6Xh ;  %'   *5<[[5Gw{{51\&-kk2L&M")++.K"L$+KK0M$N>Ekk+>V'++k":\l!#"$ H FF8$IIKJJx(-4[[9S-TH*)05R)SH&+2;;7T+UH(IIK#+;; 		  77		#//<<I NN12xx,66-4[[-?w{{5)\6=kk+6N'++k2Td 7  eg  &18U1C7;;u-")++.H"I%kk*GH ',I J:A++k:Rgkk+6Xh  x 		

8$)05O)P&%,[[1N%O"'.{{3P'Q$		#KKR8M --0=0C0CC0HV1AGGIQWWYV	VMD%=90=b1AsASXYX_X_XaQWWYb	b	{{7#'OO  T]  ah  al  al  mt  au  Tu  yB  FO  yO[[!7C";;'>C!++&>Ckk"?@$KKKKs4::<'89 JJy1$,"#"# ( 1#??,#??,D" FF4LIIKJJt!|%2 GG (%'!	&" )*		  77		#//<<I KK0145:yABBk Wbs1   S=^?]7]7'^]<]<+]<=H^z/call/namesroles/{launch_id}c                   K   t        t        j                               }t        j	                  |       }|st        dd      |d   }t        ||j	                  d            }|st        dd      |j	                  di       }|j	                  d	      }|st        dd
      d| dd}g }|}	t        j                         4 d{   }
|	r|
j	                  |	|d       d{   }	 |j                          |j                         }|j                  |j	                  dg              |j                  j	                  d      }|r0d|v r,ddl}|j!                  d|      }|r|j#                  d      nd}	nd}	|	rddd      d{    t%        |t'        |      d      S 7 7 # t        j                  $ r t        dd|j                         w xY w7 P# 1 d{  7  sw Y   `xY ww)z:Canvas: Call Names & Roles (NRPS) with pagination support.r   Launch not foundr   r   r   r   No access token availabler  r  r  r!  z8application/vnd.ims.lti-nrps.v2.membershipcontainer+jsonr   AcceptN      $@r   timeoutr  zNRPS call failed: membersLinkz
rel="next"r   z<([^>]+)>; rel="next"r   )r~  count)r  r  r  r	  r   r   r   r   rS  r   HTTPStatusErrorr   rm   extendr   researchgroupr   len)r   r   r  r   r   r#  r  r   r~  r   rV  rW  r   linkr  matchs                   rF   call_names_rolesr  )  s     
elln	B


I
&C4FGG)nG#BG(<=L4OPP^`bcJ(nn-FG"4RSS #<.1LG
 G
!C  "  fjjgtjDDA[""$ 668DNN488Ir23 99==(D,		":DA(-ekk!n4%  * Gc'lCDD+D (( [#>PQRQWQWPX<YZZ[   ss   CG3	F'
G3G'F)(G-F+=BG G3GG3)G+.GGG3G0$G'%G0,G3z/call/ags/{launch_id}c                   K   t        t        j                               }t        j	                  |       }|st        dd      |d   }t        ||j	                  d            }|st        dd      |j	                  di       }|j	                  d	      }|st        dd
      d| dd}t        j                         4 d{   }|j	                  ||d       d{   }		 |	j                          ddd      d{    t        	j                               S 7 Z7 @# t        j                  $ r t        dd|	j                         w xY w7 R# 1 d{  7  sw Y   bxY ww)z&Canvas: Fetch all line items from AGS.r   rw  r   r   r   r   rx  6https://purl.imsglobal.org/spec/lti-ags/claim/endpoint	lineitems No AGS lineitems URL in id_tokenr!  z1application/vnd.ims.lis.v2.lineitemcontainer+jsonry  Nr{  r|  r  zAGS call failed: )r  r  r  r	  r   r   r   r   rS  r   r  r   r   rm   )
r   r   r  r   r   	ags_claimlineitems_urlr   rV  rW  s
             rF   call_agsr  Y  sh     
elln	B


I
&C4FGG)nG#BG(<=L4OPPTVXYIMM+.M4VWW #<.1EG
   " V Vf**]GT*JJ	V V V !!VJ $$ 	VC:KAFF88TUU	V	V V V Vsl   CE,D E,	E!D""E'D$7E,EE,"E$.EEE,E)E E)%E,z/call/ags/{launch_id}/createzDemo Assignmentd   labelscore_maximumc                    K   t        t        j                               }t        j	                  |       }|st        dd      |d   }t        ||j	                  d            }|st        dd      |j	                  di       }|j	                  d	      }|st        dd
      d| dd}	||t        t        j                               d}
t        j                         4 d{   }|j                  ||	|
d       d{   }	 |j                          ddd      d{    t!        j#                               S 7 [7 @# t        j                  $ r t        dd|j                         w xY w7 R# 1 d{  7  sw Y   bxY ww)z#Canvas: Create a new AGS line item.r   rw  r   r   r   r   rx  r  r  r  r!  z(application/vnd.ims.lis.v2.lineitem+jsonr   zContent-Type)scoreMaximumr  
resourceIdNr{  r   rm   r}  r  zCreate line item failed: )r  r  r  r	  r   r   r   r   r   r   r   rS  r   r   r  r   r   rm   )r   r  r  r   r  r   r   r  r  r   	line_itemrV  rW  s                rF   create_line_itemr  z  s     
elln	B


I
&C4FGG)nG#BG(<=L4OPPTVXYIMM+.M4VWW #<.1BG &$**,'I   " ^ ^f++mW9VZ+[[	^ ^ ^ !!^[ $$ 	^C:STUTZTZS[8\]]	^	^ ^ ^ ^sl   C$F&E'F*E9EE9	EF$E7%FE9.E44E97F9F?F FFz/call/ags/{launch_id}/scoreP   lineitem_urlr   score_givenc                   K   t        t        j                               }t        j	                  |       }|st        dd      |d   }t        ||j	                  d            }|st        dd      |dz   }	d	| d
d}
|t        j                  dt        j                               ||ddd}t        j                         4 d{   }|j                  |	|
|d       d{   }	 |j                          ddd      d{    t!        j#                               S 7 [7 @# t        j                  $ r t        dd|j                         w xY w7 R# 1 d{  7  sw Y   bxY ww)z/Canvas: Post a score to a line item for a user.r   rw  r   r   r   r   rx  z/scoresr!  z%application/vnd.ims.lis.v1.score+jsonr  z%Y-%m-%dT%H:%M:%SZ	CompletedFullyGraded)userIdr   
scoreGivenr  activityProgressgradingProgressNr{  r  r  zPost score failed: )r  r  r  r	  r   r   r   rz   strftimegmtimer   rS  r   r   r  r   r   rm   )r   r  r   r  r  r   r  r   r   	score_urlr   score_payloadrV  rW  s                 rF   
post_scorer    sq     
elln	B


I
&C4FGG)nG#BG(<=L4OPPy(I #<.1?G ]]#7G!%'(M   " X Xf++i}VZ+[[	X X X !!X[ $$ 	XC:MaffX8VWW	X	X X X Xsl   CE/D#E/E$D%%E*D':E/EE/%E'.EEE/E, E#!E,(E/z/deep_linkingc                    K   t         j                  |       }|st        dd      |d   }|j                  di       }|j                  d      }|st        dd      d	|  d
}t        |      S w)z\Starts a Deep Linking flow from tool side (this endpoint would show a UI to select content).r   rw  r   r   9https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking
return_urlr   &No deep_linking return_url in id_tokenz
    <h3>Deep Linking - Select Resource</h3>
    <form action="/canvas/deep_linking/complete" method="post">
      <input type="hidden" name="launch_id" value="aP  "/>
      <label>Title: <input name="title" value="Demo Activity"/></label><br/>
      <label>URL: <input name="url" value="https://tool.example/activity/123"/></label><br/>
      <label>Text: <input name="text" value="A demo deep link item"/></label><br/>
      <input type="submit" value="Send deep_linking_response">
    </form>
    )r	  r   r   r   )r   r  r   dl_claimdeep_link_return_urlr  s         rF   deep_linking_startr    s      

I
&C4FGG)nG{{VXZ[H#<<54\]]4 5>; ?	D s   A-A/z/deep_linking/completezDemo Activityz'http://localhost:8000/canvas/launch/123r  r   c                   K   t         j                  |       }|st        dd      |d   }|j                  di       }|j                  d      }|st        dd      t               }|d	z   }	d
||d||dddid}
t        |j                  d      ||	t        t        j                               dd|
g|j                  d      d	}t        j                  t        j                  j                  t        j                  j                  t        j                               }ddi}t!        j"                  ||d|      }d| d| d}t%        |      S w)zG
    Canvas: Build Deep Linking Response JWT and return to Canvas.
    r   rw  r   r   r  r  r   r  iX  ltiResourceLinkz Launch this activity in the tool)r  r  tool_settingexample)r   r  r   r   lineItemcustomr   LtiDeepLinkingResponsez1.3.0r   )	r   r   r   r   r4  z6https://purl.imsglobal.org/spec/lti/claim/message_typez1https://purl.imsglobal.org/spec/lti/claim/versionz:https://purl.imsglobal.org/spec/lti-dl/claim/content_itemsr   rO   rI   r6   rT   r   zS
    <html>
      <body onload="document.forms[0].submit()">
        <form action="zB" method="POST">
          <input type="hidden" name="JWT" value="z"/>
          <noscript>
            <p>JavaScript is disabled. Please click below to continue.</p>
            <button type="submit">Continue</button>
          </noscript>
        </form>
      </body>
    </html>
    )r	  r   r   r|   r&   r   r   r   r   rc   r   rd   re   rf   rg   rh   r   r   r   )r   r  r   r  r  r   r  r  r   r   content_itemmessager   r   deep_linking_jwtr  s                   rF   deep_linking_completer    s{     

I
&C4FGG)nG {{VXZ[H#<<54\]] %C
)C "2)

 I
L {{5!TZZ\"BZ=DGSnV$
G 
#
#''++**00*779 $ C
 l#Gzz'3'7S ,, -22B1C D	D s   EEz/_debug/registrationsc                       t        t              S rx   )r   r	  r{   rH   rF   
debug_regsr  1  s    &&rH   z/_debug/access_tokenc                  X   t        t        d      5 } t        j                  | j	                         d t                     }d d d        j                         j                         }ddddt        |j                        t        |j                        d}t        |      S # 1 sw Y   axY w)Nrs   rt   rS   rT   rU   r6   rV   )r`   ra   r   load_pem_private_keyreadr   rj   rk   rG   rZ   r[   r   )key_filero   rq   r   s       rF   debug_tokenr  6  s    		% 
#88MMO#%

 $$&557G ##C !
 
s   /B  B))r6   )__doc__r  rm   rA   rz   r   loggingr   r   typingr   r   r   urllib.parser   fastapir	   r
   r   r   r   r   r   fastapi.responsesr   r   r   fastapi.staticfilesr   starlette.datastructuresr   pydanticr   joser   r   
jose.utilsr   )cryptography.hazmat.primitives.asymmetricr   cryptography.hazmat.primitivesr   r   r   cryptography.hazmat.backendsr   r   r   db_config.modelsr   r    r!   r"   r#   r$   db_config.databasedatabaser  dotenvr%   routerdependencies.canvas_configr&   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   basicConfigINFO	getLoggerrE  r   r  r2   makedirsr4   ra   rl   ry   r   rG   rr   r`   r  r  r  r   rj   rk   PUBLIC_NUMBERSrZ   r[   r   r	  rv   rH  r|   r   r   r   r   r   r   r   	api_router  r&  rB  rD  r   r[  ru  r  r  floatr  r  r  r  r  r  r{   rH   rF   <module>r     s0   
      ( & & " W W W J J + (   ' 9 @ = 8  ! b b "  	      ',, '			8	$ryy2 L4 ( ryy!7G "^1%9$:; LS LS L
%c %P 

D! X4-44!K '')88: 	^%%	&	^%%	& !T#s(^  
S Hs Hs Hc HVY H<	 	(= 	@3 @3 @Fb&3 b&N L. . N& &. )eV_5Y Y 6Yx! ! !& -%9C&C& :C&L Y   P*P* P*d CyMCMC  MCd *+-Ec -E ,-E^ #$"c " %"@ +,8<=N8Ohlmphq ""c ""# ""`e "" -""H *+9=cSWX[S\swxzs{  UY  Z]  U^ #" #"3 #"S #"kp #"  MR #" ,#"P O  4 %&#Yo&=>9	HHH 
H 	H 'HZ #$' %' "# $E s   ")O22O<