users_router.py 63 KB

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