From 7f53a3a58a63a3ae81389bb31899f5f0787cb777 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 21 Apr 2023 10:51:43 -0500 Subject: [PATCH 01/16] add new routes placeholder --- backend/app/routers/users.py | 51 +++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index 9c2ee94cc..10dcc24e5 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -1,11 +1,11 @@ +from datetime import timedelta +from secrets import token_urlsafe from typing import List + from bson import ObjectId from fastapi import APIRouter, HTTPException, Depends -from pymongo import MongoClient -from datetime import datetime, timedelta from itsdangerous.url_safe import URLSafeSerializer -from itsdangerous.exc import BadSignature -from secrets import token_urlsafe +from pymongo import MongoClient from app import dependencies from app.config import settings @@ -17,7 +17,7 @@ @router.get("", response_model=List[UserOut]) async def get_users( - db: MongoClient = Depends(dependencies.get_db), skip: int = 0, limit: int = 2 + db: MongoClient = Depends(dependencies.get_db), skip: int = 0, limit: int = 2 ): users = [] for doc in await db["users"].find().skip(skip).limit(limit).to_list(length=limit): @@ -34,18 +34,35 @@ async def get_user(user_id: str, db: MongoClient = Depends(dependencies.get_db)) @router.get("/username/{username}", response_model=UserOut) async def get_user_by_name( - username: str, db: MongoClient = Depends(dependencies.get_db) + username: str, db: MongoClient = Depends(dependencies.get_db) ): if (user := await db["users"].find_one({"email": username})) is not None: return UserOut.from_mongo(user) raise HTTPException(status_code=404, detail=f"User {username} not found") +@router.get("/keys", response_model=List[UserAPIKey]) +async def generate_user_api_key( + db: MongoClient = Depends(dependencies.get_db), + current_user=Depends(get_current_username), + skip: int = 0, + limit: int = 10, +): + """List all api keys that user has created + + Arguments: + skip -- number of page to skip + limit -- number to limit per page + """ + # TODO + return [] + + @router.post("/keys", response_model=str) async def generate_user_api_key( - mins: int = settings.local_auth_expiration, - db: MongoClient = Depends(dependencies.get_db), - current_user=Depends(get_current_username), + mins: int = settings.local_auth_expiration, + db: MongoClient = Depends(dependencies.get_db), + current_user=Depends(get_current_username), ): """Generate an API key that confers the user's privileges. @@ -62,3 +79,19 @@ async def generate_user_api_key( db["user_keys"].insert_one(user_key.to_mongo()) return hashed_key + + +@router.delete("/keys/{key_id}", response_model=str) +async def generate_user_api_key( + key_id: str, + db: MongoClient = Depends(dependencies.get_db), + current_user=Depends(get_current_username), +): + """Delete API keys given ID + + Arguments: + id -- number of minutes before expiration (0 for no expiration) + """ + # TODO + + return From 082fc03ec15cb1dc04ee46f9dc256df9a468648c Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 21 Apr 2023 15:22:24 -0500 Subject: [PATCH 02/16] add list and delete endpoint; inline docs --- backend/app/models/users.py | 4 +++- backend/app/routers/users.py | 42 ++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/backend/app/models/users.py b/backend/app/models/users.py index 21fb5322b..ce4c211fe 100644 --- a/backend/app/models/users.py +++ b/backend/app/models/users.py @@ -1,5 +1,6 @@ -from typing import Optional from datetime import datetime +from typing import Optional + from passlib.context import CryptContext from pydantic import Field, EmailStr, BaseModel from pymongo import MongoClient @@ -43,6 +44,7 @@ class UserAPIKey(MongoModel): """API keys can have a reference name (e.g. 'Uploader script')""" key: str + name: str user: EmailStr created: datetime = Field(default_factory=datetime.utcnow) expires: Optional[datetime] = None diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index 10dcc24e5..0981749b9 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -5,7 +5,7 @@ from bson import ObjectId from fastapi import APIRouter, HTTPException, Depends from itsdangerous.url_safe import URLSafeSerializer -from pymongo import MongoClient +from pymongo import MongoClient, DESCENDING from app import dependencies from app.config import settings @@ -51,15 +51,25 @@ async def generate_user_api_key( """List all api keys that user has created Arguments: - skip -- number of page to skip - limit -- number to limit per page + skip: number of page to skip + limit: number to limit per page """ - # TODO - return [] + apikeys = [] + for doc in ( + await db["user_keys"].find({"user": current_user}) + .sort([("created", DESCENDING)]) + .skip(skip) + .limit(limit) + .to_list(length=limit) + ): + apikeys.append(UserAPIKey.from_mongo(doc)) + + return apikeys @router.post("/keys", response_model=str) async def generate_user_api_key( + name: str, mins: int = settings.local_auth_expiration, db: MongoClient = Depends(dependencies.get_db), current_user=Depends(get_current_username), @@ -67,13 +77,14 @@ async def generate_user_api_key( """Generate an API key that confers the user's privileges. Arguments: - mins -- number of minutes before expiration (0 for no expiration) + name: name of the api key + mins: number of minutes before expiration (0 for no expiration) """ serializer = URLSafeSerializer(settings.local_auth_secret, salt="api_key") unique_key = token_urlsafe(16) hashed_key = serializer.dumps({"user": current_user, "key": unique_key}) - user_key = UserAPIKey(user=current_user, key=unique_key) + user_key = UserAPIKey(user=current_user, key=unique_key, name=name) if mins > 0: user_key.expires = user_key.created + timedelta(minutes=mins) db["user_keys"].insert_one(user_key.to_mongo()) @@ -90,8 +101,17 @@ async def generate_user_api_key( """Delete API keys given ID Arguments: - id -- number of minutes before expiration (0 for no expiration) + key_id: id of the apikey """ - # TODO - - return + apikey_doc = (await db["user_keys"].find_one({"_id": ObjectId(key_id)})) + if apikey_doc is not None: + apikey = UserAPIKey.from_mongo(apikey_doc) + + # Only allow user to delete their own key + if apikey.user == current_user: + await db["user_keys"].delete_one({"_id": ObjectId(key_id)}) + return {"deleted": apikey} + else: + raise HTTPException(status_code=403, detail=f"API key {key_id} not allowed to be deleted.") + else: + raise HTTPException(status_code=404, detail=f"API key {key_id} not found.") From 0b9c36bfe70775b6bbbdcc60fec1551d01f91972 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 21 Apr 2023 16:19:29 -0500 Subject: [PATCH 03/16] codegen and pytest --- backend/app/tests/test_apikey.py | 19 ++++++ backend/app/tests/utils.py | 38 ++++++++---- frontend/src/actions/user.js | 8 +-- frontend/src/openapi/v2/index.ts | 1 + frontend/src/openapi/v2/models/UserAPIKey.ts | 15 +++++ .../src/openapi/v2/services/UsersService.ts | 58 ++++++++++++++++++- 6 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 backend/app/tests/test_apikey.py create mode 100644 frontend/src/openapi/v2/models/UserAPIKey.ts diff --git a/backend/app/tests/test_apikey.py b/backend/app/tests/test_apikey.py new file mode 100644 index 000000000..58f0deb12 --- /dev/null +++ b/backend/app/tests/test_apikey.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient + +from app.config import settings +from app.tests.utils import create_apikey + + +def test_create_apikey(client: TestClient, headers: dict): + key_id = create_apikey(client, headers).get("id") + response = client.get(f"{settings.API_V2_STR}/users/keys/{key_id}", headers=headers) + assert response.status_code == 200 + assert response.json().get("id") is not None + + +def test_list_apikeys(client: TestClient, headers: dict): + pass + + +def test_delete_apikeys(client: TestClient, headers: dict): + pass diff --git a/backend/app/tests/utils.py b/backend/app/tests/utils.py index c7c45d32b..fcc3cfb12 100644 --- a/backend/app/tests/utils.py +++ b/backend/app/tests/utils.py @@ -1,5 +1,7 @@ import os + from fastapi.testclient import TestClient + from app.config import settings """These are standard JSON entries to be used for creating test resources.""" @@ -69,7 +71,6 @@ "bibtex": [], } - """CONVENIENCE FUNCTIONS FOR COMMON ACTIONS REQUIRED BY TESTS.""" @@ -79,7 +80,7 @@ def create_user(client: TestClient, headers: dict, email: str = user_alt["email" u["email"] = email response = client.post(f"{settings.API_V2_STR}/users", json=u) assert ( - response.status_code == 200 or response.status_code == 409 + response.status_code == 200 or response.status_code == 409 ) # 409 = user already exists return response.json() @@ -95,6 +96,19 @@ def get_user_token(client: TestClient, headers: dict, email: str = user_alt["ema return {"Authorization": "Bearer " + token} +def create_apikey(client: TestClient, headers: dict): + """create user generated API key""" + response = client.post( + f"{settings.API_V2_STR}/users/keys", json={ + "name": "pytest API key", + "mins": 30 # 30 minutes + }, headers=headers + ) + assert response.status_code == 200 + assert response.json().get("id") is not None + return response.json() + + def create_group(client: TestClient, headers: dict): """Creates a test group (creator will be auto-added to members) and returns the JSON.""" response = client.post( @@ -116,11 +130,11 @@ def create_dataset(client: TestClient, headers: dict): def upload_file( - client: TestClient, - headers: dict, - dataset_id: str, - filename=filename_example, - content=file_content_example, + client: TestClient, + headers: dict, + dataset_id: str, + filename=filename_example, + content=file_content_example, ): """Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON.""" with open(filename, "w") as tempf: @@ -138,11 +152,11 @@ def upload_file( def create_folder( - client: TestClient, - headers: dict, - dataset_id: str, - name="test folder", - parent_folder=None, + client: TestClient, + headers: dict, + dataset_id: str, + name="test folder", + parent_folder=None, ): """Creates a folder (optionally under an existing folder) in a dataset and returns the JSON.""" folder_data = {"name": name} diff --git a/frontend/src/actions/user.js b/frontend/src/actions/user.js index 2841f3879..418d480d6 100644 --- a/frontend/src/actions/user.js +++ b/frontend/src/actions/user.js @@ -126,16 +126,16 @@ export function fetchAllUsers(skip = 0, limit = 101) { }); }) .catch((reason) => { - dispatch(fetchAllUsers((skip = 0), (limit = 21))); + dispatch(fetchAllUsers(skip, limit)); }); }; } export const GENERATE_API_KEY = "GENERATE_API_KEY"; -export function generateApiKey(minutes = 30) { +export function generateApiKey(name = "", minutes = 30) { return (dispatch) => { - return V2.UsersService.generateUserApiKeyApiV2UsersKeysPost(minutes) + return V2.UsersService.generateUserApiKeyApiV2UsersKeysPost(name, minutes) .then((json) => { dispatch({ type: GENERATE_API_KEY, @@ -144,7 +144,7 @@ export function generateApiKey(minutes = 30) { }); }) .catch((reason) => { - dispatch(generateApiKey((minutes = 30))); + dispatch(generateApiKey(name, minutes)); }); }; } diff --git a/frontend/src/openapi/v2/index.ts b/frontend/src/openapi/v2/index.ts index 1013bfa85..03c8c1c19 100644 --- a/frontend/src/openapi/v2/index.ts +++ b/frontend/src/openapi/v2/index.ts @@ -52,6 +52,7 @@ export { RoleType } from './models/RoleType'; export type { SearchCriteria } from './models/SearchCriteria'; export type { SearchObject } from './models/SearchObject'; export type { UserAndRole } from './models/UserAndRole'; +export type { UserAPIKey } from './models/UserAPIKey'; export type { UserIn } from './models/UserIn'; export type { UserLogin } from './models/UserLogin'; export type { UserOut } from './models/UserOut'; diff --git a/frontend/src/openapi/v2/models/UserAPIKey.ts b/frontend/src/openapi/v2/models/UserAPIKey.ts new file mode 100644 index 000000000..80d974e24 --- /dev/null +++ b/frontend/src/openapi/v2/models/UserAPIKey.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * API keys can have a reference name (e.g. 'Uploader script') + */ +export type UserAPIKey = { + id?: string; + key: string; + name: string; + user: string; + created?: string; + expires?: string; +} diff --git a/frontend/src/openapi/v2/services/UsersService.ts b/frontend/src/openapi/v2/services/UsersService.ts index 21ca92613..e7659503a 100644 --- a/frontend/src/openapi/v2/services/UsersService.ts +++ b/frontend/src/openapi/v2/services/UsersService.ts @@ -1,6 +1,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { UserAPIKey } from '../models/UserAPIKey'; import type { UserOut } from '../models/UserOut'; import type { CancelablePromise } from '../core/CancelablePromise'; import { request as __request } from '../core/request'; @@ -67,23 +68,56 @@ export class UsersService { }); } + /** + * Generate User Api Key + * List all api keys that user has created + * + * Arguments: + * skip: number of page to skip + * limit: number to limit per page + * @param skip + * @param limit + * @returns UserAPIKey Successful Response + * @throws ApiError + */ + public static generateUserApiKeyApiV2UsersKeysGet( + skip?: number, + limit: number = 10, + ): CancelablePromise> { + return __request({ + method: 'GET', + path: `/api/v2/users/keys`, + query: { + 'skip': skip, + 'limit': limit, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** * Generate User Api Key * Generate an API key that confers the user's privileges. * * Arguments: - * mins -- number of minutes before expiration (0 for no expiration) + * name: name of the api key + * mins: number of minutes before expiration (0 for no expiration) + * @param name * @param mins * @returns string Successful Response * @throws ApiError */ public static generateUserApiKeyApiV2UsersKeysPost( + name: string, mins: number = 30, ): CancelablePromise { return __request({ method: 'POST', path: `/api/v2/users/keys`, query: { + 'name': name, 'mins': mins, }, errors: { @@ -92,4 +126,26 @@ export class UsersService { }); } + /** + * Generate User Api Key + * Delete API keys given ID + * + * Arguments: + * key_id: id of the apikey + * @param keyId + * @returns string Successful Response + * @throws ApiError + */ + public static generateUserApiKeyApiV2UsersKeysKeyIdDelete( + keyId: string, + ): CancelablePromise { + return __request({ + method: 'DELETE', + path: `/api/v2/users/keys/${keyId}`, + errors: { + 422: `Validation Error`, + }, + }); + } + } \ No newline at end of file From b55d9f8d2b77bfa8c2b1934c439f6e13af2c5731 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 21 Apr 2023 16:28:49 -0500 Subject: [PATCH 04/16] ugly ui? --- backend/app/tests/utils.py | 5 +---- frontend/src/components/users/ApiKeyModal.tsx | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/app/tests/utils.py b/backend/app/tests/utils.py index fcc3cfb12..38f5256ed 100644 --- a/backend/app/tests/utils.py +++ b/backend/app/tests/utils.py @@ -99,10 +99,7 @@ def get_user_token(client: TestClient, headers: dict, email: str = user_alt["ema def create_apikey(client: TestClient, headers: dict): """create user generated API key""" response = client.post( - f"{settings.API_V2_STR}/users/keys", json={ - "name": "pytest API key", - "mins": 30 # 30 minutes - }, headers=headers + f"{settings.API_V2_STR}/users/keys?name=pytest&mins=30", headers=headers ) assert response.status_code == 200 assert response.json().get("id") is not None diff --git a/frontend/src/components/users/ApiKeyModal.tsx b/frontend/src/components/users/ApiKeyModal.tsx index b03ffb5fc..f11748128 100644 --- a/frontend/src/components/users/ApiKeyModal.tsx +++ b/frontend/src/components/users/ApiKeyModal.tsx @@ -19,6 +19,8 @@ import { RootState } from "../../types/data"; import { ClowderMetadataTextField } from "../styledComponents/ClowderMetadataTextField"; import { ClowderFootnote } from "../styledComponents/ClowderFootnote"; import { CopyToClipboard } from "react-copy-to-clipboard"; +import { ClowderInputLabel } from "../styledComponents/ClowderInputLabel"; +import { ClowderInput } from "../styledComponents/ClowderInput"; type ApiKeyModalProps = { apiKeyModalOpen: boolean; @@ -29,11 +31,12 @@ export const ApiKeyModal = (props: ApiKeyModalProps) => { const { apiKeyModalOpen, setApiKeyModalOpen } = props; const dispatch = useDispatch(); - const generateApiKey = (minutes: number) => - dispatch(generateApiKeyAction(minutes)); + const generateApiKey = (name: string, minutes: number) => + dispatch(generateApiKeyAction(name, minutes)); const resetApiKey = () => dispatch(resetApiKeyAction()); const apiKey = useSelector((state: RootState) => state.user.apiKey); + const [name, setName] = useState(""); const [minutes, setMinutes] = useState(30); const handleClose = () => { @@ -42,7 +45,7 @@ export const ApiKeyModal = (props: ApiKeyModalProps) => { }; const handleGenerate = () => { - generateApiKey(minutes); + generateApiKey(name, minutes); }; const handleExpirationChange = (e) => { @@ -77,6 +80,15 @@ export const ApiKeyModal = (props: ApiKeyModalProps) => { Your API key will expire + Name + { + setName(event.target.value); + }} + defaultValue={name} + /> After 30 Minutes From 2c032c6f824d90e293f05199a4af750f38193033 Mon Sep 17 00:00:00 2001 From: toddn Date: Mon, 24 Apr 2023 16:54:18 -0500 Subject: [PATCH 16/16] formatting --- backend/app/models/users.py | 1 + backend/app/routers/users.py | 43 +++++++++++++++++--------------- backend/app/tests/test_apikey.py | 4 ++- backend/app/tests/utils.py | 22 ++++++++-------- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/backend/app/models/users.py b/backend/app/models/users.py index 7e5d677c0..cadb7c900 100644 --- a/backend/app/models/users.py +++ b/backend/app/models/users.py @@ -42,6 +42,7 @@ class UserOut(UserBase): class UserAPIKey(MongoModel): """API keys can have a reference name (e.g. 'Uploader script')""" + key: str name: str user: EmailStr diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index a274e1a90..478b6b14e 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -17,10 +17,10 @@ @router.get("/keys", response_model=List[UserAPIKeyOut]) async def generate_user_api_key( - db: MongoClient = Depends(dependencies.get_db), - current_user=Depends(get_current_username), - skip: int = 0, - limit: int = 10, + db: MongoClient = Depends(dependencies.get_db), + current_user=Depends(get_current_username), + skip: int = 0, + limit: int = 10, ): """List all api keys that user has created @@ -30,11 +30,12 @@ async def generate_user_api_key( """ apikeys = [] for doc in ( - await db["user_keys"].find({"user": current_user}) - .sort([("created", DESCENDING)]) - .skip(skip) - .limit(limit) - .to_list(length=limit) + await db["user_keys"] + .find({"user": current_user}) + .sort([("created", DESCENDING)]) + .skip(skip) + .limit(limit) + .to_list(length=limit) ): apikeys.append(UserAPIKeyOut.from_mongo(doc)) @@ -43,10 +44,10 @@ async def generate_user_api_key( @router.post("/keys", response_model=str) async def generate_user_api_key( - name: str, - mins: int = settings.local_auth_expiration, - db: MongoClient = Depends(dependencies.get_db), - current_user=Depends(get_current_username), + name: str, + mins: int = settings.local_auth_expiration, + db: MongoClient = Depends(dependencies.get_db), + current_user=Depends(get_current_username), ): """Generate an API key that confers the user's privileges. @@ -68,16 +69,16 @@ async def generate_user_api_key( @router.delete("/keys/{key_id}", response_model=UserAPIKeyOut) async def generate_user_api_key( - key_id: str, - db: MongoClient = Depends(dependencies.get_db), - current_user=Depends(get_current_username), + key_id: str, + db: MongoClient = Depends(dependencies.get_db), + current_user=Depends(get_current_username), ): """Delete API keys given ID Arguments: key_id: id of the apikey """ - apikey_doc = (await db["user_keys"].find_one({"_id": ObjectId(key_id)})) + apikey_doc = await db["user_keys"].find_one({"_id": ObjectId(key_id)}) if apikey_doc is not None: apikey = UserAPIKeyOut.from_mongo(apikey_doc) @@ -86,14 +87,16 @@ async def generate_user_api_key( await db["user_keys"].delete_one({"_id": ObjectId(key_id)}) return apikey else: - raise HTTPException(status_code=403, detail=f"API key {key_id} not allowed to be deleted.") + raise HTTPException( + status_code=403, detail=f"API key {key_id} not allowed to be deleted." + ) else: raise HTTPException(status_code=404, detail=f"API key {key_id} not found.") @router.get("", response_model=List[UserOut]) async def get_users( - db: MongoClient = Depends(dependencies.get_db), skip: int = 0, limit: int = 2 + db: MongoClient = Depends(dependencies.get_db), skip: int = 0, limit: int = 2 ): users = [] for doc in await db["users"].find().skip(skip).limit(limit).to_list(length=limit): @@ -110,7 +113,7 @@ async def get_user(user_id: str, db: MongoClient = Depends(dependencies.get_db)) @router.get("/username/{username}", response_model=UserOut) async def get_user_by_name( - username: str, db: MongoClient = Depends(dependencies.get_db) + username: str, db: MongoClient = Depends(dependencies.get_db) ): if (user := await db["users"].find_one({"email": username})) is not None: return UserOut.from_mongo(user) diff --git a/backend/app/tests/test_apikey.py b/backend/app/tests/test_apikey.py index 2a8daf0b9..a525f03ab 100644 --- a/backend/app/tests/test_apikey.py +++ b/backend/app/tests/test_apikey.py @@ -18,5 +18,7 @@ def test_delete_apikeys(client: TestClient, headers: dict): create_apikey(client, headers) get_response = client.get(f"{settings.API_V2_STR}/users/keys", headers=headers) key_id = get_response.json()[0].get("id") - delete_response = client.delete(f"{settings.API_V2_STR}/users/keys/{key_id}", headers=headers) + delete_response = client.delete( + f"{settings.API_V2_STR}/users/keys/{key_id}", headers=headers + ) assert delete_response.status_code == 200 diff --git a/backend/app/tests/utils.py b/backend/app/tests/utils.py index 7fd5dcaaf..cef6bf3df 100644 --- a/backend/app/tests/utils.py +++ b/backend/app/tests/utils.py @@ -80,7 +80,7 @@ def create_user(client: TestClient, headers: dict, email: str = user_alt["email" u["email"] = email response = client.post(f"{settings.API_V2_STR}/users", json=u) assert ( - response.status_code == 200 or response.status_code == 409 + response.status_code == 200 or response.status_code == 409 ) # 409 = user already exists return response.json() @@ -127,11 +127,11 @@ def create_dataset(client: TestClient, headers: dict): def upload_file( - client: TestClient, - headers: dict, - dataset_id: str, - filename=filename_example, - content=file_content_example, + client: TestClient, + headers: dict, + dataset_id: str, + filename=filename_example, + content=file_content_example, ): """Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON.""" with open(filename, "w") as tempf: @@ -149,11 +149,11 @@ def upload_file( def create_folder( - client: TestClient, - headers: dict, - dataset_id: str, - name="test folder", - parent_folder=None, + client: TestClient, + headers: dict, + dataset_id: str, + name="test folder", + parent_folder=None, ): """Creates a folder (optionally under an existing folder) in a dataset and returns the JSON.""" folder_data = {"name": name}