users_router.py 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712
  1. import logging
  2. import os
  3. import textwrap
  4. import urllib.parse
  5. from typing import Optional
  6. from uuid import UUID
  7. import requests
  8. from fastapi import Body, Depends, HTTPException, Path, Query
  9. from fastapi.background import BackgroundTasks
  10. from fastapi.responses import FileResponse
  11. from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
  12. from google.auth.transport import requests as google_requests
  13. from google.oauth2 import id_token
  14. from pydantic import EmailStr
  15. from core.base import R2RException
  16. from core.base.api.models import (
  17. GenericBooleanResponse,
  18. GenericMessageResponse,
  19. WrappedAPIKeyResponse,
  20. WrappedAPIKeysResponse,
  21. WrappedBooleanResponse,
  22. WrappedCollectionsResponse,
  23. WrappedGenericMessageResponse,
  24. WrappedLimitsResponse,
  25. WrappedLoginResponse,
  26. WrappedTokenResponse,
  27. WrappedUserResponse,
  28. WrappedUsersResponse,
  29. )
  30. from ...abstractions import R2RProviders, R2RServices
  31. from ...config import R2RConfig
  32. from .base_router import BaseRouterV3
  33. oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  34. class UsersRouter(BaseRouterV3):
  35. def __init__(
  36. self, providers: R2RProviders, services: R2RServices, config: R2RConfig
  37. ):
  38. logging.info("Initializing UsersRouter")
  39. super().__init__(providers, services, config)
  40. self.google_client_id = os.environ.get("GOOGLE_CLIENT_ID")
  41. self.google_client_secret = os.environ.get("GOOGLE_CLIENT_SECRET")
  42. self.google_redirect_uri = os.environ.get("GOOGLE_REDIRECT_URI")
  43. self.github_client_id = os.environ.get("GITHUB_CLIENT_ID")
  44. self.github_client_secret = os.environ.get("GITHUB_CLIENT_SECRET")
  45. self.github_redirect_uri = os.environ.get("GITHUB_REDIRECT_URI")
  46. def _setup_routes(self):
  47. @self.router.post(
  48. "/users",
  49. # dependencies=[Depends(self.rate_limit_dependency)],
  50. response_model=WrappedUserResponse,
  51. openapi_extra={
  52. "x-codeSamples": [
  53. {
  54. "lang": "Python",
  55. "source": textwrap.dedent("""
  56. from r2r import R2RClient
  57. client = R2RClient()
  58. new_user = client.users.create(
  59. email="jane.doe@example.com",
  60. password="secure_password123"
  61. )"""),
  62. },
  63. {
  64. "lang": "JavaScript",
  65. "source": textwrap.dedent("""
  66. const { r2rClient } = require("r2r-js");
  67. const client = new r2rClient();
  68. function main() {
  69. const response = await client.users.create({
  70. email: "jane.doe@example.com",
  71. password: "secure_password123"
  72. });
  73. }
  74. main();
  75. """),
  76. },
  77. {
  78. "lang": "cURL",
  79. "source": textwrap.dedent("""
  80. curl -X POST "https://api.example.com/v3/users" \\
  81. -H "Content-Type: application/json" \\
  82. -d '{
  83. "email": "jane.doe@example.com",
  84. "password": "secure_password123"
  85. }'"""),
  86. },
  87. ]
  88. },
  89. )
  90. @self.base_endpoint
  91. async def register(
  92. email: EmailStr = Body(..., description="User's email address"),
  93. password: str = Body(..., description="User's password"),
  94. name: str | None = Body(
  95. None, description="The name for the new user"
  96. ),
  97. bio: str | None = Body(
  98. None, description="The bio for the new user"
  99. ),
  100. profile_picture: str | None = Body(
  101. None, description="Updated user profile picture"
  102. ),
  103. is_verified: bool = Body(
  104. False,
  105. description="Whether to verify the user immediately",
  106. ),
  107. auth_user=Depends(self.providers.auth.auth_wrapper()),
  108. ) -> WrappedUserResponse:
  109. """Register a new user with the given email and password."""
  110. if is_verified and not auth_user.is_superuser:
  111. raise R2RException(
  112. "Non-superuser cannot verify users during registration.",
  113. 403,
  114. )
  115. registration_response = await self.services.auth.register(
  116. email=email,
  117. password=password,
  118. is_verified=is_verified,
  119. name=name,
  120. bio=bio,
  121. profile_picture=profile_picture,
  122. )
  123. return registration_response # type: ignore
  124. @self.router.post(
  125. "/users/export",
  126. summary="Export users to CSV",
  127. dependencies=[Depends(self.rate_limit_dependency)],
  128. openapi_extra={
  129. "x-codeSamples": [
  130. {
  131. "lang": "Python",
  132. "source": textwrap.dedent("""
  133. from r2r import R2RClient
  134. client = R2RClient("http://localhost:7272")
  135. # when using auth, do client.login(...)
  136. response = client.users.export(
  137. output_path="export.csv",
  138. columns=["id", "name", "created_at"],
  139. include_header=True,
  140. )
  141. """),
  142. },
  143. {
  144. "lang": "JavaScript",
  145. "source": textwrap.dedent("""
  146. const { r2rClient } = require("r2r-js");
  147. const client = new r2rClient("http://localhost:7272");
  148. function main() {
  149. await client.users.export({
  150. outputPath: "export.csv",
  151. columns: ["id", "name", "created_at"],
  152. includeHeader: true,
  153. });
  154. }
  155. main();
  156. """),
  157. },
  158. {
  159. "lang": "cURL",
  160. "source": textwrap.dedent("""
  161. curl -X POST "http://127.0.0.1:7272/v3/users/export" \
  162. -H "Authorization: Bearer YOUR_API_KEY" \
  163. -H "Content-Type: application/json" \
  164. -H "Accept: text/csv" \
  165. -d '{ "columns": ["id", "name", "created_at"], "include_header": true }' \
  166. --output export.csv
  167. """),
  168. },
  169. ]
  170. },
  171. )
  172. @self.base_endpoint
  173. async def export_users(
  174. background_tasks: BackgroundTasks,
  175. columns: Optional[list[str]] = Body(
  176. None, description="Specific columns to export"
  177. ),
  178. filters: Optional[dict] = Body(
  179. None, description="Filters to apply to the export"
  180. ),
  181. include_header: Optional[bool] = Body(
  182. True, description="Whether to include column headers"
  183. ),
  184. auth_user=Depends(self.providers.auth.auth_wrapper()),
  185. ) -> FileResponse:
  186. """Export users as a CSV file."""
  187. if not auth_user.is_superuser:
  188. raise R2RException(
  189. status_code=403,
  190. message="Only a superuser can export data.",
  191. )
  192. (
  193. csv_file_path,
  194. temp_file,
  195. ) = await self.services.management.export_users(
  196. columns=columns,
  197. filters=filters,
  198. include_header=include_header
  199. if include_header is not None
  200. else True,
  201. )
  202. background_tasks.add_task(temp_file.close)
  203. return FileResponse(
  204. path=csv_file_path,
  205. media_type="text/csv",
  206. filename="users_export.csv",
  207. )
  208. @self.router.post(
  209. "/users/verify-email",
  210. # dependencies=[Depends(self.rate_limit_dependency)],
  211. response_model=WrappedGenericMessageResponse,
  212. openapi_extra={
  213. "x-codeSamples": [
  214. {
  215. "lang": "Python",
  216. "source": textwrap.dedent("""
  217. from r2r import R2RClient
  218. client = R2RClient()
  219. tokens = client.users.verify_email(
  220. email="jane.doe@example.com",
  221. verification_code="1lklwal!awdclm"
  222. )"""),
  223. },
  224. {
  225. "lang": "JavaScript",
  226. "source": textwrap.dedent("""
  227. const { r2rClient } = require("r2r-js");
  228. const client = new r2rClient();
  229. function main() {
  230. const response = await client.users.verifyEmail({
  231. email: jane.doe@example.com",
  232. verificationCode: "1lklwal!awdclm"
  233. });
  234. }
  235. main();
  236. """),
  237. },
  238. {
  239. "lang": "cURL",
  240. "source": textwrap.dedent("""
  241. curl -X POST "https://api.example.com/v3/users/login" \\
  242. -H "Content-Type: application/x-www-form-urlencoded" \\
  243. -d "email=jane.doe@example.com&verification_code=1lklwal!awdclm"
  244. """),
  245. },
  246. ]
  247. },
  248. )
  249. @self.base_endpoint
  250. async def verify_email(
  251. email: EmailStr = Body(..., description="User's email address"),
  252. verification_code: str = Body(
  253. ..., description="Email verification code"
  254. ),
  255. ) -> WrappedGenericMessageResponse:
  256. """Verify a user's email address."""
  257. user = (
  258. await self.providers.database.users_handler.get_user_by_email(
  259. email
  260. )
  261. )
  262. if user and user.is_verified:
  263. raise R2RException(
  264. status_code=400,
  265. message="This email is already verified. Please log in.",
  266. )
  267. result = await self.services.auth.verify_email(
  268. email, verification_code
  269. )
  270. return GenericMessageResponse(message=result["message"]) # type: ignore
  271. @self.router.post(
  272. "/users/send-verification-email",
  273. dependencies=[
  274. Depends(self.providers.auth.auth_wrapper(public=True))
  275. ],
  276. response_model=WrappedGenericMessageResponse,
  277. openapi_extra={
  278. "x-codeSamples": [
  279. {
  280. "lang": "Python",
  281. "source": textwrap.dedent("""
  282. from r2r import R2RClient
  283. client = R2RClient()
  284. tokens = client.users.send_verification_email(
  285. email="jane.doe@example.com",
  286. )"""),
  287. },
  288. {
  289. "lang": "JavaScript",
  290. "source": textwrap.dedent("""
  291. const { r2rClient } = require("r2r-js");
  292. const client = new r2rClient();
  293. function main() {
  294. const response = await client.users.sendVerificationEmail({
  295. email: jane.doe@example.com",
  296. });
  297. }
  298. main();
  299. """),
  300. },
  301. {
  302. "lang": "cURL",
  303. "source": textwrap.dedent("""
  304. curl -X POST "https://api.example.com/v3/users/send-verification-email" \\
  305. -H "Content-Type: application/x-www-form-urlencoded" \\
  306. -d "email=jane.doe@example.com"
  307. """),
  308. },
  309. ]
  310. },
  311. )
  312. @self.base_endpoint
  313. async def send_verification_email(
  314. email: EmailStr = Body(..., description="User's email address"),
  315. ) -> WrappedGenericMessageResponse:
  316. """Send a user's email a verification code."""
  317. user = (
  318. await self.providers.database.users_handler.get_user_by_email(
  319. email
  320. )
  321. )
  322. if user and user.is_verified:
  323. raise R2RException(
  324. status_code=400,
  325. message="This email is already verified. Please log in.",
  326. )
  327. await self.services.auth.send_verification_email(email=email)
  328. return GenericMessageResponse(
  329. message="A verification email has been sent."
  330. ) # type: ignore
  331. @self.router.post(
  332. "/users/login",
  333. # dependencies=[Depends(self.rate_limit_dependency)],
  334. response_model=WrappedTokenResponse,
  335. openapi_extra={
  336. "x-codeSamples": [
  337. {
  338. "lang": "Python",
  339. "source": textwrap.dedent("""
  340. from r2r import R2RClient
  341. client = R2RClient()
  342. tokens = client.users.login(
  343. email="jane.doe@example.com",
  344. password="secure_password123"
  345. )
  346. """),
  347. },
  348. {
  349. "lang": "JavaScript",
  350. "source": textwrap.dedent("""
  351. const { r2rClient } = require("r2r-js");
  352. const client = new r2rClient();
  353. function main() {
  354. const response = await client.users.login({
  355. email: jane.doe@example.com",
  356. password: "secure_password123"
  357. });
  358. }
  359. main();
  360. """),
  361. },
  362. {
  363. "lang": "cURL",
  364. "source": textwrap.dedent("""
  365. curl -X POST "https://api.example.com/v3/users/login" \\
  366. -H "Content-Type: application/x-www-form-urlencoded" \\
  367. -d "username=jane.doe@example.com&password=secure_password123"
  368. """),
  369. },
  370. ]
  371. },
  372. )
  373. @self.base_endpoint
  374. async def login(
  375. form_data: OAuth2PasswordRequestForm = Depends(),
  376. ) -> WrappedLoginResponse:
  377. """Authenticate a user and provide access tokens."""
  378. return await self.services.auth.login( # type: ignore
  379. form_data.username, form_data.password
  380. )
  381. @self.router.post(
  382. "/users/logout",
  383. response_model=WrappedGenericMessageResponse,
  384. openapi_extra={
  385. "x-codeSamples": [
  386. {
  387. "lang": "Python",
  388. "source": textwrap.dedent("""
  389. from r2r import R2RClient
  390. client = R2RClient()
  391. # client.login(...)
  392. result = client.users.logout()
  393. """),
  394. },
  395. {
  396. "lang": "JavaScript",
  397. "source": textwrap.dedent("""
  398. const { r2rClient } = require("r2r-js");
  399. const client = new r2rClient();
  400. function main() {
  401. const response = await client.users.logout();
  402. }
  403. main();
  404. """),
  405. },
  406. {
  407. "lang": "cURL",
  408. "source": textwrap.dedent("""
  409. curl -X POST "https://api.example.com/v3/users/logout" \\
  410. -H "Authorization: Bearer YOUR_API_KEY"
  411. """),
  412. },
  413. ]
  414. },
  415. )
  416. @self.base_endpoint
  417. async def logout(
  418. token: str = Depends(oauth2_scheme),
  419. auth_user=Depends(self.providers.auth.auth_wrapper()),
  420. ) -> WrappedGenericMessageResponse:
  421. """Log out the current user."""
  422. result = await self.services.auth.logout(token)
  423. return GenericMessageResponse(message=result["message"]) # type: ignore
  424. @self.router.post(
  425. "/users/refresh-token",
  426. # dependencies=[Depends(self.rate_limit_dependency)],
  427. openapi_extra={
  428. "x-codeSamples": [
  429. {
  430. "lang": "Python",
  431. "source": textwrap.dedent("""
  432. from r2r import R2RClient
  433. client = R2RClient()
  434. # client.login(...)
  435. new_tokens = client.users.refresh_token()
  436. # New tokens are automatically stored in the client"""),
  437. },
  438. {
  439. "lang": "JavaScript",
  440. "source": textwrap.dedent("""
  441. const { r2rClient } = require("r2r-js");
  442. const client = new r2rClient();
  443. function main() {
  444. const response = await client.users.refreshAccessToken();
  445. }
  446. main();
  447. """),
  448. },
  449. {
  450. "lang": "cURL",
  451. "source": textwrap.dedent("""
  452. curl -X POST "https://api.example.com/v3/users/refresh-token" \\
  453. -H "Content-Type: application/json" \\
  454. -d '{
  455. "refresh_token": "YOUR_REFRESH_TOKEN"
  456. }'"""),
  457. },
  458. ]
  459. },
  460. )
  461. @self.base_endpoint
  462. async def refresh_token(
  463. refresh_token: str = Body(..., description="Refresh token"),
  464. ) -> WrappedTokenResponse:
  465. """Refresh the access token using a refresh token."""
  466. result = await self.services.auth.refresh_access_token(
  467. refresh_token=refresh_token
  468. )
  469. return result # type: ignore
  470. @self.router.post(
  471. "/users/change-password",
  472. dependencies=[Depends(self.rate_limit_dependency)],
  473. response_model=WrappedGenericMessageResponse,
  474. openapi_extra={
  475. "x-codeSamples": [
  476. {
  477. "lang": "Python",
  478. "source": textwrap.dedent("""
  479. from r2r import R2RClient
  480. client = R2RClient()
  481. # client.login(...)
  482. result = client.users.change_password(
  483. current_password="old_password123",
  484. new_password="new_secure_password456"
  485. )"""),
  486. },
  487. {
  488. "lang": "JavaScript",
  489. "source": textwrap.dedent("""
  490. const { r2rClient } = require("r2r-js");
  491. const client = new r2rClient();
  492. function main() {
  493. const response = await client.users.changePassword({
  494. currentPassword: "old_password123",
  495. newPassword: "new_secure_password456"
  496. });
  497. }
  498. main();
  499. """),
  500. },
  501. {
  502. "lang": "cURL",
  503. "source": textwrap.dedent("""
  504. curl -X POST "https://api.example.com/v3/users/change-password" \\
  505. -H "Authorization: Bearer YOUR_API_KEY" \\
  506. -H "Content-Type: application/json" \\
  507. -d '{
  508. "current_password": "old_password123",
  509. "new_password": "new_secure_password456"
  510. }'"""),
  511. },
  512. ]
  513. },
  514. )
  515. @self.base_endpoint
  516. async def change_password(
  517. current_password: str = Body(..., description="Current password"),
  518. new_password: str = Body(..., description="New password"),
  519. auth_user=Depends(self.providers.auth.auth_wrapper()),
  520. ) -> WrappedGenericMessageResponse:
  521. """Change the authenticated user's password."""
  522. result = await self.services.auth.change_password(
  523. auth_user, current_password, new_password
  524. )
  525. return GenericMessageResponse(message=result["message"]) # type: ignore
  526. @self.router.post(
  527. "/users/request-password-reset",
  528. dependencies=[
  529. Depends(self.providers.auth.auth_wrapper(public=True))
  530. ],
  531. response_model=WrappedGenericMessageResponse,
  532. openapi_extra={
  533. "x-codeSamples": [
  534. {
  535. "lang": "Python",
  536. "source": textwrap.dedent("""
  537. from r2r import R2RClient
  538. client = R2RClient()
  539. result = client.users.request_password_reset(
  540. email="jane.doe@example.com"
  541. )"""),
  542. },
  543. {
  544. "lang": "JavaScript",
  545. "source": textwrap.dedent("""
  546. const { r2rClient } = require("r2r-js");
  547. const client = new r2rClient();
  548. function main() {
  549. const response = await client.users.requestPasswordReset({
  550. email: jane.doe@example.com",
  551. });
  552. }
  553. main();
  554. """),
  555. },
  556. {
  557. "lang": "cURL",
  558. "source": textwrap.dedent("""
  559. curl -X POST "https://api.example.com/v3/users/request-password-reset" \\
  560. -H "Content-Type: application/json" \\
  561. -d '{
  562. "email": "jane.doe@example.com"
  563. }'"""),
  564. },
  565. ]
  566. },
  567. )
  568. @self.base_endpoint
  569. async def request_password_reset(
  570. email: EmailStr = Body(..., description="User's email address"),
  571. ) -> WrappedGenericMessageResponse:
  572. """Request a password reset for a user."""
  573. result = await self.services.auth.request_password_reset(email)
  574. return GenericMessageResponse(message=result["message"]) # type: ignore
  575. @self.router.post(
  576. "/users/reset-password",
  577. dependencies=[
  578. Depends(self.providers.auth.auth_wrapper(public=True))
  579. ],
  580. response_model=WrappedGenericMessageResponse,
  581. openapi_extra={
  582. "x-codeSamples": [
  583. {
  584. "lang": "Python",
  585. "source": textwrap.dedent("""
  586. from r2r import R2RClient
  587. client = R2RClient()
  588. result = client.users.reset_password(
  589. reset_token="reset_token_received_via_email",
  590. new_password="new_secure_password789"
  591. )"""),
  592. },
  593. {
  594. "lang": "JavaScript",
  595. "source": textwrap.dedent("""
  596. const { r2rClient } = require("r2r-js");
  597. const client = new r2rClient();
  598. function main() {
  599. const response = await client.users.resetPassword({
  600. resestToken: "reset_token_received_via_email",
  601. newPassword: "new_secure_password789"
  602. });
  603. }
  604. main();
  605. """),
  606. },
  607. {
  608. "lang": "cURL",
  609. "source": textwrap.dedent("""
  610. curl -X POST "https://api.example.com/v3/users/reset-password" \\
  611. -H "Content-Type: application/json" \\
  612. -d '{
  613. "reset_token": "reset_token_received_via_email",
  614. "new_password": "new_secure_password789"
  615. }'"""),
  616. },
  617. ]
  618. },
  619. )
  620. @self.base_endpoint
  621. async def reset_password(
  622. reset_token: str = Body(..., description="Password reset token"),
  623. new_password: str = Body(..., description="New password"),
  624. ) -> WrappedGenericMessageResponse:
  625. """Reset a user's password using a reset token."""
  626. result = await self.services.auth.confirm_password_reset(
  627. reset_token, new_password
  628. )
  629. return GenericMessageResponse(message=result["message"]) # type: ignore
  630. @self.router.get(
  631. "/users",
  632. dependencies=[Depends(self.rate_limit_dependency)],
  633. summary="List Users",
  634. openapi_extra={
  635. "x-codeSamples": [
  636. {
  637. "lang": "Python",
  638. "source": textwrap.dedent("""
  639. from r2r import R2RClient
  640. client = R2RClient()
  641. # client.login(...)
  642. # List users with filters
  643. users = client.users.list(
  644. offset=0,
  645. limit=100,
  646. )
  647. """),
  648. },
  649. {
  650. "lang": "JavaScript",
  651. "source": textwrap.dedent("""
  652. const { r2rClient } = require("r2r-js");
  653. const client = new r2rClient();
  654. function main() {
  655. const response = await client.users.list();
  656. }
  657. main();
  658. """),
  659. },
  660. {
  661. "lang": "Shell",
  662. "source": textwrap.dedent("""
  663. curl -X GET "https://api.example.com/users?offset=0&limit=100&username=john&email=john@example.com&is_active=true&is_superuser=false" \\
  664. -H "Authorization: Bearer YOUR_API_KEY"
  665. """),
  666. },
  667. ]
  668. },
  669. )
  670. @self.base_endpoint
  671. async def list_users(
  672. ids: list[str] = Query(
  673. [], description="List of user IDs to filter by"
  674. ),
  675. offset: int = Query(
  676. 0,
  677. ge=0,
  678. description="Specifies the number of objects to skip. Defaults to 0.",
  679. ),
  680. limit: int = Query(
  681. 100,
  682. ge=1,
  683. le=1000,
  684. description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
  685. ),
  686. auth_user=Depends(self.providers.auth.auth_wrapper()),
  687. ) -> WrappedUsersResponse:
  688. """List all users with pagination and filtering options.
  689. Only accessible by superusers.
  690. """
  691. if not auth_user.is_superuser:
  692. raise R2RException(
  693. status_code=403,
  694. message="Only a superuser can call the `users_overview` endpoint.",
  695. )
  696. user_uuids = [UUID(user_id) for user_id in ids]
  697. users_overview_response = (
  698. await self.services.management.users_overview(
  699. user_ids=user_uuids, offset=offset, limit=limit
  700. )
  701. )
  702. return users_overview_response["results"], { # type: ignore
  703. "total_entries": users_overview_response["total_entries"]
  704. }
  705. @self.router.get(
  706. "/users/me",
  707. dependencies=[Depends(self.rate_limit_dependency)],
  708. summary="Get the Current User",
  709. openapi_extra={
  710. "x-codeSamples": [
  711. {
  712. "lang": "Python",
  713. "source": textwrap.dedent("""
  714. from r2r import R2RClient
  715. client = R2RClient()
  716. # client.login(...)
  717. # Get user details
  718. users = client.users.me()
  719. """),
  720. },
  721. {
  722. "lang": "JavaScript",
  723. "source": textwrap.dedent("""
  724. const { r2rClient } = require("r2r-js");
  725. const client = new r2rClient();
  726. function main() {
  727. const response = await client.users.me();
  728. }
  729. main();
  730. """),
  731. },
  732. {
  733. "lang": "Shell",
  734. "source": textwrap.dedent("""
  735. curl -X GET "https://api.example.com/users/me" \\
  736. -H "Authorization: Bearer YOUR_API_KEY"
  737. """),
  738. },
  739. ]
  740. },
  741. )
  742. @self.base_endpoint
  743. async def get_current_user(
  744. auth_user=Depends(self.providers.auth.auth_wrapper()),
  745. ) -> WrappedUserResponse:
  746. """Get detailed information about the currently authenticated
  747. user."""
  748. return auth_user
  749. @self.router.get(
  750. "/users/{id}",
  751. dependencies=[Depends(self.rate_limit_dependency)],
  752. summary="Get User Details",
  753. openapi_extra={
  754. "x-codeSamples": [
  755. {
  756. "lang": "Python",
  757. "source": textwrap.dedent("""
  758. from r2r import R2RClient
  759. client = R2RClient()
  760. # client.login(...)
  761. # Get user details
  762. users = client.users.retrieve(
  763. id="b4ac4dd6-5f27-596e-a55b-7cf242ca30aa"
  764. )
  765. """),
  766. },
  767. {
  768. "lang": "JavaScript",
  769. "source": textwrap.dedent("""
  770. const { r2rClient } = require("r2r-js");
  771. const client = new r2rClient();
  772. function main() {
  773. const response = await client.users.retrieve({
  774. id: "b4ac4dd6-5f27-596e-a55b-7cf242ca30aa"
  775. });
  776. }
  777. main();
  778. """),
  779. },
  780. {
  781. "lang": "Shell",
  782. "source": textwrap.dedent("""
  783. curl -X GET "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000" \\
  784. -H "Authorization: Bearer YOUR_API_KEY"
  785. """),
  786. },
  787. ]
  788. },
  789. )
  790. @self.base_endpoint
  791. async def get_user(
  792. id: UUID = Path(
  793. ..., example="550e8400-e29b-41d4-a716-446655440000"
  794. ),
  795. auth_user=Depends(self.providers.auth.auth_wrapper()),
  796. ) -> WrappedUserResponse:
  797. """Get detailed information about a specific user.
  798. Users can only access their own information unless they are
  799. superusers.
  800. """
  801. if not auth_user.is_superuser and auth_user.id != id:
  802. raise R2RException(
  803. "Only a superuser can call the get `user` endpoint for other users.",
  804. 403,
  805. )
  806. users_overview_response = (
  807. await self.services.management.users_overview(
  808. offset=0,
  809. limit=1,
  810. user_ids=[id],
  811. )
  812. )
  813. return users_overview_response["results"][0]
  814. @self.router.delete(
  815. "/users/{id}",
  816. dependencies=[Depends(self.rate_limit_dependency)],
  817. summary="Delete User",
  818. openapi_extra={
  819. "x-codeSamples": [
  820. {
  821. "lang": "Python",
  822. "source": textwrap.dedent("""
  823. from r2r import R2RClient
  824. client = R2RClient()
  825. # client.login(...)
  826. # Delete user
  827. client.users.delete(id="550e8400-e29b-41d4-a716-446655440000", password="secure_password123")
  828. """),
  829. },
  830. {
  831. "lang": "JavaScript",
  832. "source": textwrap.dedent("""
  833. const { r2rClient } = require("r2r-js");
  834. const client = new r2rClient();
  835. function main() {
  836. const response = await client.users.delete({
  837. id: "550e8400-e29b-41d4-a716-446655440000",
  838. password: "secure_password123"
  839. });
  840. }
  841. main();
  842. """),
  843. },
  844. ]
  845. },
  846. )
  847. @self.base_endpoint
  848. async def delete_user(
  849. id: UUID = Path(
  850. ..., example="550e8400-e29b-41d4-a716-446655440000"
  851. ),
  852. password: Optional[str] = Body(
  853. None, description="User's current password"
  854. ),
  855. delete_vector_data: Optional[bool] = Body(
  856. False,
  857. description="Whether to delete the user's vector data",
  858. ),
  859. auth_user=Depends(self.providers.auth.auth_wrapper()),
  860. ) -> WrappedBooleanResponse:
  861. """Delete a specific user.
  862. Users can only delete their own account unless they are superusers.
  863. """
  864. if not auth_user.is_superuser and auth_user.id != id:
  865. raise R2RException(
  866. "Only a superuser can delete other users.",
  867. 403,
  868. )
  869. await self.services.auth.delete_user(
  870. user_id=id,
  871. password=password,
  872. delete_vector_data=delete_vector_data or False,
  873. is_superuser=auth_user.is_superuser,
  874. )
  875. return GenericBooleanResponse(success=True) # type: ignore
  876. @self.router.get(
  877. "/users/{id}/collections",
  878. dependencies=[Depends(self.rate_limit_dependency)],
  879. summary="Get User Collections",
  880. openapi_extra={
  881. "x-codeSamples": [
  882. {
  883. "lang": "Python",
  884. "source": textwrap.dedent("""
  885. from r2r import R2RClient
  886. client = R2RClient()
  887. # client.login(...)
  888. # Get user collections
  889. collections = client.user.list_collections(
  890. "550e8400-e29b-41d4-a716-446655440000",
  891. offset=0,
  892. limit=100
  893. )
  894. """),
  895. },
  896. {
  897. "lang": "JavaScript",
  898. "source": textwrap.dedent("""
  899. const { r2rClient } = require("r2r-js");
  900. const client = new r2rClient();
  901. function main() {
  902. const response = await client.users.listCollections({
  903. id: "550e8400-e29b-41d4-a716-446655440000",
  904. offset: 0,
  905. limit: 100
  906. });
  907. }
  908. main();
  909. """),
  910. },
  911. {
  912. "lang": "Shell",
  913. "source": textwrap.dedent("""
  914. curl -X GET "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/collections?offset=0&limit=100" \\
  915. -H "Authorization: Bearer YOUR_API_KEY"
  916. """),
  917. },
  918. ]
  919. },
  920. )
  921. @self.base_endpoint
  922. async def get_user_collections(
  923. id: UUID = Path(
  924. ..., example="550e8400-e29b-41d4-a716-446655440000"
  925. ),
  926. offset: int = Query(
  927. 0,
  928. ge=0,
  929. description="Specifies the number of objects to skip. Defaults to 0.",
  930. ),
  931. limit: int = Query(
  932. 100,
  933. ge=1,
  934. le=1000,
  935. description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
  936. ),
  937. auth_user=Depends(self.providers.auth.auth_wrapper()),
  938. ) -> WrappedCollectionsResponse:
  939. """Get all collections associated with a specific user.
  940. Users can only access their own collections unless they are
  941. superusers.
  942. """
  943. if auth_user.id != id and not auth_user.is_superuser:
  944. raise R2RException(
  945. "The currently authenticated user does not have access to the specified collection.",
  946. 403,
  947. )
  948. user_collection_response = (
  949. await self.services.management.collections_overview(
  950. offset=offset,
  951. limit=limit,
  952. user_ids=[id],
  953. )
  954. )
  955. return user_collection_response["results"], { # type: ignore
  956. "total_entries": user_collection_response["total_entries"]
  957. }
  958. @self.router.post(
  959. "/users/{id}/collections/{collection_id}",
  960. dependencies=[Depends(self.rate_limit_dependency)],
  961. summary="Add User to Collection",
  962. response_model=WrappedBooleanResponse,
  963. openapi_extra={
  964. "x-codeSamples": [
  965. {
  966. "lang": "Python",
  967. "source": textwrap.dedent("""
  968. from r2r import R2RClient
  969. client = R2RClient()
  970. # client.login(...)
  971. # Add user to collection
  972. client.users.add_to_collection(
  973. id="550e8400-e29b-41d4-a716-446655440000",
  974. collection_id="750e8400-e29b-41d4-a716-446655440000"
  975. )
  976. """),
  977. },
  978. {
  979. "lang": "JavaScript",
  980. "source": textwrap.dedent("""
  981. const { r2rClient } = require("r2r-js");
  982. const client = new r2rClient();
  983. function main() {
  984. const response = await client.users.addToCollection({
  985. id: "550e8400-e29b-41d4-a716-446655440000",
  986. collectionId: "750e8400-e29b-41d4-a716-446655440000"
  987. });
  988. }
  989. main();
  990. """),
  991. },
  992. {
  993. "lang": "Shell",
  994. "source": textwrap.dedent("""
  995. curl -X POST "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/collections/750e8400-e29b-41d4-a716-446655440000" \\
  996. -H "Authorization: Bearer YOUR_API_KEY"
  997. """),
  998. },
  999. ]
  1000. },
  1001. )
  1002. @self.base_endpoint
  1003. async def add_user_to_collection(
  1004. id: UUID = Path(
  1005. ..., example="550e8400-e29b-41d4-a716-446655440000"
  1006. ),
  1007. collection_id: UUID = Path(
  1008. ..., example="750e8400-e29b-41d4-a716-446655440000"
  1009. ),
  1010. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1011. ) -> WrappedBooleanResponse:
  1012. if auth_user.id != id and not auth_user.is_superuser:
  1013. raise R2RException(
  1014. "The currently authenticated user does not have access to the specified collection.",
  1015. 403,
  1016. )
  1017. # TODO - Do we need a check on user access to the collection?
  1018. await self.services.management.add_user_to_collection( # type: ignore
  1019. id, collection_id
  1020. )
  1021. return GenericBooleanResponse(success=True) # type: ignore
  1022. @self.router.delete(
  1023. "/users/{id}/collections/{collection_id}",
  1024. dependencies=[Depends(self.rate_limit_dependency)],
  1025. summary="Remove User from Collection",
  1026. openapi_extra={
  1027. "x-codeSamples": [
  1028. {
  1029. "lang": "Python",
  1030. "source": textwrap.dedent("""
  1031. from r2r import R2RClient
  1032. client = R2RClient()
  1033. # client.login(...)
  1034. # Remove user from collection
  1035. client.users.remove_from_collection(
  1036. id="550e8400-e29b-41d4-a716-446655440000",
  1037. collection_id="750e8400-e29b-41d4-a716-446655440000"
  1038. )
  1039. """),
  1040. },
  1041. {
  1042. "lang": "JavaScript",
  1043. "source": textwrap.dedent("""
  1044. const { r2rClient } = require("r2r-js");
  1045. const client = new r2rClient();
  1046. function main() {
  1047. const response = await client.users.removeFromCollection({
  1048. id: "550e8400-e29b-41d4-a716-446655440000",
  1049. collectionId: "750e8400-e29b-41d4-a716-446655440000"
  1050. });
  1051. }
  1052. main();
  1053. """),
  1054. },
  1055. {
  1056. "lang": "Shell",
  1057. "source": textwrap.dedent("""
  1058. curl -X DELETE "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/collections/750e8400-e29b-41d4-a716-446655440000" \\
  1059. -H "Authorization: Bearer YOUR_API_KEY"
  1060. """),
  1061. },
  1062. ]
  1063. },
  1064. )
  1065. @self.base_endpoint
  1066. async def remove_user_from_collection(
  1067. id: UUID = Path(
  1068. ..., example="550e8400-e29b-41d4-a716-446655440000"
  1069. ),
  1070. collection_id: UUID = Path(
  1071. ..., example="750e8400-e29b-41d4-a716-446655440000"
  1072. ),
  1073. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1074. ) -> WrappedBooleanResponse:
  1075. """Remove a user from a collection.
  1076. Requires either superuser status or access to the collection.
  1077. """
  1078. if auth_user.id != id and not auth_user.is_superuser:
  1079. raise R2RException(
  1080. "The currently authenticated user does not have access to the specified collection.",
  1081. 403,
  1082. )
  1083. # TODO - Do we need a check on user access to the collection?
  1084. await self.services.management.remove_user_from_collection( # type: ignore
  1085. id, collection_id
  1086. )
  1087. return GenericBooleanResponse(success=True) # type: ignore
  1088. @self.router.post(
  1089. "/users/{id}",
  1090. dependencies=[Depends(self.rate_limit_dependency)],
  1091. summary="Update User",
  1092. openapi_extra={
  1093. "x-codeSamples": [
  1094. {
  1095. "lang": "Python",
  1096. "source": textwrap.dedent("""
  1097. from r2r import R2RClient
  1098. client = R2RClient()
  1099. # client.login(...)
  1100. # Update user
  1101. updated_user = client.update_user(
  1102. "550e8400-e29b-41d4-a716-446655440000",
  1103. name="John Doe"
  1104. )
  1105. """),
  1106. },
  1107. {
  1108. "lang": "JavaScript",
  1109. "source": textwrap.dedent("""
  1110. const { r2rClient } = require("r2r-js");
  1111. const client = new r2rClient();
  1112. function main() {
  1113. const response = await client.users.update({
  1114. id: "550e8400-e29b-41d4-a716-446655440000",
  1115. name: "John Doe"
  1116. });
  1117. }
  1118. main();
  1119. """),
  1120. },
  1121. {
  1122. "lang": "Shell",
  1123. "source": textwrap.dedent("""
  1124. curl -X POST "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000" \\
  1125. -H "Authorization: Bearer YOUR_API_KEY" \\
  1126. -H "Content-Type: application/json" \\
  1127. -d '{
  1128. "id": "550e8400-e29b-41d4-a716-446655440000",
  1129. "name": "John Doe",
  1130. }'
  1131. """),
  1132. },
  1133. ]
  1134. },
  1135. )
  1136. # TODO - Modify update user to have synced params with user object
  1137. @self.base_endpoint
  1138. async def update_user(
  1139. id: UUID = Path(..., description="ID of the user to update"),
  1140. email: EmailStr | None = Body(
  1141. None, description="Updated email address"
  1142. ),
  1143. is_superuser: bool | None = Body(
  1144. None, description="Updated superuser status"
  1145. ),
  1146. name: str | None = Body(None, description="Updated user name"),
  1147. bio: str | None = Body(None, description="Updated user bio"),
  1148. profile_picture: str | None = Body(
  1149. None, description="Updated profile picture URL"
  1150. ),
  1151. limits_overrides: dict = Body(
  1152. None,
  1153. description="Updated limits overrides",
  1154. ),
  1155. metadata: dict[str, str | None] | None = None,
  1156. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1157. ) -> WrappedUserResponse:
  1158. """Update user information.
  1159. Users can only update their own information unless they are
  1160. superusers. Superuser status can only be modified by existing
  1161. superusers.
  1162. """
  1163. if is_superuser is not None and not auth_user.is_superuser:
  1164. raise R2RException(
  1165. "Only superusers can update the superuser status of a user",
  1166. 403,
  1167. )
  1168. if not auth_user.is_superuser and auth_user.id != id:
  1169. raise R2RException(
  1170. "Only superusers can update other users' information",
  1171. 403,
  1172. )
  1173. if not auth_user.is_superuser and limits_overrides is not None:
  1174. raise R2RException(
  1175. "Only superusers can update other users' limits overrides",
  1176. 403,
  1177. )
  1178. # Pass `metadata` to our auth or management service so it can do a
  1179. # partial (Stripe-like) merge of metadata.
  1180. return await self.services.auth.update_user( # type: ignore
  1181. user_id=id,
  1182. email=email,
  1183. is_superuser=is_superuser,
  1184. name=name,
  1185. bio=bio,
  1186. profile_picture=profile_picture,
  1187. limits_overrides=limits_overrides,
  1188. new_metadata=metadata,
  1189. )
  1190. @self.router.post(
  1191. "/users/{id}/api-keys",
  1192. dependencies=[Depends(self.rate_limit_dependency)],
  1193. summary="Create User API Key",
  1194. response_model=WrappedAPIKeyResponse,
  1195. openapi_extra={
  1196. "x-codeSamples": [
  1197. {
  1198. "lang": "Python",
  1199. "source": textwrap.dedent("""
  1200. from r2r import R2RClient
  1201. client = R2RClient()
  1202. # client.login(...)
  1203. result = client.users.create_api_key(
  1204. id="550e8400-e29b-41d4-a716-446655440000",
  1205. name="My API Key",
  1206. description="API key for accessing the app",
  1207. )
  1208. # result["api_key"] contains the newly created API key
  1209. """),
  1210. },
  1211. {
  1212. "lang": "cURL",
  1213. "source": textwrap.dedent("""
  1214. curl -X POST "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/api-keys" \\
  1215. -H "Authorization: Bearer YOUR_API_TOKEN" \\
  1216. -d '{"name": "My API Key", "description": "API key for accessing the app"}'
  1217. """),
  1218. },
  1219. ]
  1220. },
  1221. )
  1222. @self.base_endpoint
  1223. async def create_user_api_key(
  1224. id: UUID = Path(
  1225. ..., description="ID of the user for whom to create an API key"
  1226. ),
  1227. name: Optional[str] = Body(
  1228. None, description="Name of the API key"
  1229. ),
  1230. description: Optional[str] = Body(
  1231. None, description="Description of the API key"
  1232. ),
  1233. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1234. ) -> WrappedAPIKeyResponse:
  1235. """Create a new API key for the specified user.
  1236. Only superusers or the user themselves may create an API key.
  1237. """
  1238. if auth_user.id != id and not auth_user.is_superuser:
  1239. raise R2RException(
  1240. "Only the user themselves or a superuser can create API keys for this user.",
  1241. 403,
  1242. )
  1243. api_key = await self.services.auth.create_user_api_key(
  1244. id, name=name, description=description
  1245. )
  1246. return api_key # type: ignore
  1247. @self.router.get(
  1248. "/users/{id}/api-keys",
  1249. dependencies=[Depends(self.rate_limit_dependency)],
  1250. summary="List User API Keys",
  1251. openapi_extra={
  1252. "x-codeSamples": [
  1253. {
  1254. "lang": "Python",
  1255. "source": textwrap.dedent("""
  1256. from r2r import R2RClient
  1257. client = R2RClient()
  1258. # client.login(...)
  1259. keys = client.users.list_api_keys(
  1260. id="550e8400-e29b-41d4-a716-446655440000"
  1261. )
  1262. """),
  1263. },
  1264. {
  1265. "lang": "cURL",
  1266. "source": textwrap.dedent("""
  1267. curl -X GET "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/api-keys" \\
  1268. -H "Authorization: Bearer YOUR_API_TOKEN"
  1269. """),
  1270. },
  1271. ]
  1272. },
  1273. )
  1274. @self.base_endpoint
  1275. async def list_user_api_keys(
  1276. id: UUID = Path(
  1277. ..., description="ID of the user whose API keys to list"
  1278. ),
  1279. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1280. ) -> WrappedAPIKeysResponse:
  1281. """List all API keys for the specified user.
  1282. Only superusers or the user themselves may list the API keys.
  1283. """
  1284. if auth_user.id != id and not auth_user.is_superuser:
  1285. raise R2RException(
  1286. "Only the user themselves or a superuser can list API keys for this user.",
  1287. 403,
  1288. )
  1289. keys = (
  1290. await self.providers.database.users_handler.get_user_api_keys(
  1291. id
  1292. )
  1293. )
  1294. return keys, {"total_entries": len(keys)} # type: ignore
  1295. @self.router.delete(
  1296. "/users/{id}/api-keys/{key_id}",
  1297. dependencies=[Depends(self.rate_limit_dependency)],
  1298. summary="Delete User API Key",
  1299. openapi_extra={
  1300. "x-codeSamples": [
  1301. {
  1302. "lang": "Python",
  1303. "source": textwrap.dedent("""
  1304. from r2r import R2RClient
  1305. from uuid import UUID
  1306. client = R2RClient()
  1307. # client.login(...)
  1308. response = client.users.delete_api_key(
  1309. id="550e8400-e29b-41d4-a716-446655440000",
  1310. key_id="d9c562d4-3aef-43e8-8f08-0cf7cd5e0a25"
  1311. )
  1312. """),
  1313. },
  1314. {
  1315. "lang": "cURL",
  1316. "source": textwrap.dedent("""
  1317. curl -X DELETE "https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/api-keys/d9c562d4-3aef-43e8-8f08-0cf7cd5e0a25" \\
  1318. -H "Authorization: Bearer YOUR_API_TOKEN"
  1319. """),
  1320. },
  1321. ]
  1322. },
  1323. )
  1324. @self.base_endpoint
  1325. async def delete_user_api_key(
  1326. id: UUID = Path(..., description="ID of the user"),
  1327. key_id: UUID = Path(
  1328. ..., description="ID of the API key to delete"
  1329. ),
  1330. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1331. ) -> WrappedBooleanResponse:
  1332. """Delete a specific API key for the specified user.
  1333. Only superusers or the user themselves may delete the API key.
  1334. """
  1335. if auth_user.id != id and not auth_user.is_superuser:
  1336. raise R2RException(
  1337. "Only the user themselves or a superuser can delete this API key.",
  1338. 403,
  1339. )
  1340. success = (
  1341. await self.providers.database.users_handler.delete_api_key(
  1342. id, key_id
  1343. )
  1344. )
  1345. if not success:
  1346. raise R2RException(
  1347. "API key not found or could not be deleted", 400
  1348. )
  1349. return {"success": True} # type: ignore
  1350. @self.router.get(
  1351. "/users/{id}/limits",
  1352. summary="Fetch User Limits",
  1353. responses={
  1354. 200: {
  1355. "description": "Returns system default limits, user overrides, and final effective settings."
  1356. },
  1357. 403: {
  1358. "description": "If the requesting user is neither the same user nor a superuser."
  1359. },
  1360. 404: {"description": "If the user ID does not exist."},
  1361. },
  1362. openapi_extra={
  1363. "x-codeSamples": [
  1364. {
  1365. "lang": "Python",
  1366. "source": """
  1367. from r2r import R2RClient
  1368. client = R2RClient()
  1369. # client.login(...)
  1370. user_limits = client.users.get_limits("550e8400-e29b-41d4-a716-446655440000")
  1371. """,
  1372. },
  1373. {
  1374. "lang": "JavaScript",
  1375. "source": """
  1376. const { r2rClient } = require("r2r-js");
  1377. const client = new r2rClient();
  1378. // await client.users.login(...)
  1379. async function main() {
  1380. const userLimits = await client.users.getLimits({
  1381. id: "550e8400-e29b-41d4-a716-446655440000"
  1382. });
  1383. console.log(userLimits);
  1384. }
  1385. main();
  1386. """,
  1387. },
  1388. {
  1389. "lang": "cURL",
  1390. "source": """
  1391. curl -X GET "https://api.example.com/v3/users/550e8400-e29b-41d4-a716-446655440000/limits" \\
  1392. -H "Authorization: Bearer YOUR_API_KEY"
  1393. """,
  1394. },
  1395. ]
  1396. },
  1397. )
  1398. @self.base_endpoint
  1399. async def get_user_limits(
  1400. id: UUID = Path(
  1401. ..., description="ID of the user to fetch limits for"
  1402. ),
  1403. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1404. ) -> WrappedLimitsResponse:
  1405. """Return the system default limits, user-level overrides, and
  1406. final "effective" limit settings for the specified user.
  1407. Only superusers or the user themself may fetch these values.
  1408. """
  1409. if (auth_user.id != id) and (not auth_user.is_superuser):
  1410. raise R2RException(
  1411. "Only the user themselves or a superuser can view these limits.",
  1412. status_code=403,
  1413. )
  1414. # This calls the new helper you created in ManagementService
  1415. limits_info = await self.services.management.get_all_user_limits(
  1416. id
  1417. )
  1418. return limits_info # type: ignore
  1419. @self.router.get("/users/oauth/google/authorize")
  1420. @self.base_endpoint
  1421. async def google_authorize() -> WrappedGenericMessageResponse:
  1422. """Redirect user to Google's OAuth 2.0 consent screen."""
  1423. state = "some_random_string_or_csrf_token" # Usually you store a random state in session/Redis
  1424. scope = "openid email profile"
  1425. # Build the Google OAuth URL
  1426. params = {
  1427. "client_id": self.google_client_id,
  1428. "redirect_uri": self.google_redirect_uri,
  1429. "response_type": "code",
  1430. "scope": scope,
  1431. "state": state,
  1432. "access_type": "offline", # to get refresh token if needed
  1433. "prompt": "consent", # Force consent each time if you want
  1434. }
  1435. google_auth_url = f"https://accounts.google.com/o/oauth2/v2/auth?{urllib.parse.urlencode(params)}"
  1436. return GenericMessageResponse(message=google_auth_url) # type: ignore
  1437. @self.router.get("/users/oauth/google/callback")
  1438. @self.base_endpoint
  1439. async def google_callback(
  1440. code: str = Query(...), state: str = Query(...)
  1441. ) -> WrappedLoginResponse:
  1442. """Google's callback that will receive the `code` and `state`.
  1443. We then exchange code for tokens, verify, and log the user in.
  1444. """
  1445. # 1. Exchange `code` for tokens
  1446. token_data = requests.post(
  1447. "https://oauth2.googleapis.com/token",
  1448. data={
  1449. "code": code,
  1450. "client_id": self.google_client_id,
  1451. "client_secret": self.google_client_secret,
  1452. "redirect_uri": self.google_redirect_uri,
  1453. "grant_type": "authorization_code",
  1454. },
  1455. ).json()
  1456. if "error" in token_data:
  1457. raise HTTPException(
  1458. status_code=400,
  1459. detail=f"Failed to get token: {token_data}",
  1460. )
  1461. # 2. Verify the ID token
  1462. id_token_str = token_data["id_token"]
  1463. try:
  1464. # google_auth.transport.requests.Request() is a session for verifying
  1465. id_info = id_token.verify_oauth2_token(
  1466. id_token_str,
  1467. google_requests.Request(),
  1468. self.google_client_id,
  1469. )
  1470. except ValueError as e:
  1471. raise HTTPException(
  1472. status_code=400,
  1473. detail=f"Token verification failed: {str(e)}",
  1474. ) from e
  1475. # id_info will contain "sub", "email", etc.
  1476. google_id = id_info["sub"]
  1477. email = id_info.get("email")
  1478. email = email or f"{google_id}@google_oauth.fake"
  1479. # 3. Now call our R2RAuthProvider method that handles "oauth-based" user creation or login
  1480. return await self.providers.auth.oauth_callback_handler( # type: ignore
  1481. provider="google",
  1482. oauth_id=google_id,
  1483. email=email,
  1484. )
  1485. @self.router.get("/users/oauth/github/authorize")
  1486. @self.base_endpoint
  1487. async def github_authorize() -> WrappedGenericMessageResponse:
  1488. """Redirect user to GitHub's OAuth consent screen."""
  1489. state = "some_random_string_or_csrf_token"
  1490. scope = "read:user user:email"
  1491. params = {
  1492. "client_id": self.github_client_id,
  1493. "redirect_uri": self.github_redirect_uri,
  1494. "scope": scope,
  1495. "state": state,
  1496. }
  1497. github_auth_url = f"https://github.com/login/oauth/authorize?{urllib.parse.urlencode(params)}"
  1498. return GenericMessageResponse(message=github_auth_url) # type: ignore
  1499. @self.router.get("/users/oauth/github/callback")
  1500. @self.base_endpoint
  1501. async def github_callback(
  1502. code: str = Query(...), state: str = Query(...)
  1503. ) -> WrappedLoginResponse:
  1504. """GitHub callback route to exchange code for an access_token, then
  1505. fetch user info from GitHub's API, then do the same 'oauth-based'
  1506. login or registration."""
  1507. # 1. Exchange code for access_token
  1508. token_resp = requests.post(
  1509. "https://github.com/login/oauth/access_token",
  1510. data={
  1511. "client_id": self.github_client_id,
  1512. "client_secret": self.github_client_secret,
  1513. "code": code,
  1514. "redirect_uri": self.github_redirect_uri,
  1515. "state": state,
  1516. },
  1517. headers={"Accept": "application/json"},
  1518. )
  1519. token_data = token_resp.json()
  1520. if "error" in token_data:
  1521. raise HTTPException(
  1522. status_code=400,
  1523. detail=f"Failed to get token: {token_data}",
  1524. )
  1525. access_token = token_data["access_token"]
  1526. # 2. Use the access_token to fetch user info
  1527. user_info_resp = requests.get(
  1528. "https://api.github.com/user",
  1529. headers={"Authorization": f"Bearer {access_token}"},
  1530. ).json()
  1531. github_id = str(
  1532. user_info_resp["id"]
  1533. ) # GitHub user ID is typically an integer
  1534. # fetch email (sometimes you need to call /user/emails endpoint if user sets email private)
  1535. email = user_info_resp.get("email")
  1536. email = email or f"{github_id}@github_oauth.fake"
  1537. # 3. Pass to your auth provider
  1538. return await self.providers.auth.oauth_callback_handler( # type: ignore
  1539. provider="github",
  1540. oauth_id=github_id,
  1541. email=email,
  1542. )