From 30064b2c9e535d3b4531ed38a95b7fb5cc683b1b Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 23 Sep 2024 18:33:50 +0300 Subject: [PATCH 01/13] chores_lint_errors_and_path_issues --- Makefile | 1 + apprunner.yaml | 2 +- debug.py | 6 ++ linguaphoto/ai/cli.py | 9 +- linguaphoto/api/collection.py | 10 ++- linguaphoto/api/image.py | 5 +- linguaphoto/api/user.py | 6 +- linguaphoto/crud/base.py | 2 +- linguaphoto/crud/collection.py | 2 +- linguaphoto/crud/image.py | 6 +- linguaphoto/crud/user.py | 5 +- linguaphoto/main.py | 87 ------------------- linguaphoto/models.py | 2 +- linguaphoto/settings.py | 4 +- linguaphoto/utils/auth.py | 2 + linguaphoto/utils/cloudfront_url_signer.py | 2 +- .../private_key.pem => private_key.pem | 0 17 files changed, 46 insertions(+), 105 deletions(-) create mode 100644 debug.py rename linguaphoto/private_key.pem => private_key.pem (100%) diff --git a/Makefile b/Makefile index 0e0e0af..3966aec 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ start-db: # ------------------------ # format-backend: + @isort linguaphoto @black linguaphoto @ruff format linguaphoto .PHONY: format diff --git a/apprunner.yaml b/apprunner.yaml index 2fa80da..0fd0c55 100644 --- a/apprunner.yaml +++ b/apprunner.yaml @@ -12,7 +12,7 @@ build: # Specify the start phase command run: - command: venv/bin/python3 main.py # Adjust to the path to your application entry point + command: venv/bin/python3 linguaphoto/main.py # Adjust to the path to your application entry point env: - name: DYNAMODB_TABLE_NAME value: "linguaphoto" diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..c404722 --- /dev/null +++ b/debug.py @@ -0,0 +1,6 @@ +"""it is entity for debugging""" +import uvicorn + +from linguaphoto.main import app + +uvicorn.run(app, port=8080, host="0.0.0.0") diff --git a/linguaphoto/ai/cli.py b/linguaphoto/ai/cli.py index 02a64b9..2df6bad 100644 --- a/linguaphoto/ai/cli.py +++ b/linguaphoto/ai/cli.py @@ -3,6 +3,7 @@ import argparse import asyncio import logging +from io import BytesIO from pathlib import Path from openai import AsyncOpenAI @@ -29,7 +30,13 @@ async def main() -> None: client = AsyncOpenAI( api_key="sk-svcacct-PFETCFHtqmHOmIpP_IAyQfBGz5LOpvC6Zudj7d5Wcdp9WjJT4ImAxuotGcpyT3BlbkFJRbtswQqIxYHam9TN13mCM04_OTZE-v8z-Rw1WEcwzyZqW_GcK0PNNyFp6BcA" ) - transcription_response = await transcribe_image(image, client) + # Convert the ImageFile to BytesIO + image_bytes = BytesIO() + image.save(image_bytes, format="JPEG") # Use the appropriate format for your image + image_bytes.seek(0) # Reset the stream position to the beginning + + # Now call the transcribe_image function with the BytesIO object + transcription_response = await transcribe_image(image_bytes, client) print(transcription_response.model_dump_json(indent=2)) with open(root_dir / "transcription.json", "w") as file: file.write(transcription_response.model_dump_json(indent=2)) diff --git a/linguaphoto/api/collection.py b/linguaphoto/api/collection.py index dc63ed4..b7bc7f7 100644 --- a/linguaphoto/api/collection.py +++ b/linguaphoto/api/collection.py @@ -7,7 +7,10 @@ from linguaphoto.crud.collection import CollectionCrud from linguaphoto.errors import NotAuthorizedError from linguaphoto.models import Collection -from linguaphoto.schemas.collection import CollectionCreateFragment, CollectionEditFragment +from linguaphoto.schemas.collection import ( + CollectionCreateFragment, + CollectionEditFragment, +) from linguaphoto.utils.auth import get_current_user_id router = APIRouter() @@ -33,10 +36,11 @@ async def create( @router.get("/get_collection", response_model=Collection) async def getcollection( id: str, user_id: str = Depends(get_current_user_id), collection_crud: CollectionCrud = Depends() -) -> dict | None: +) -> Collection: async with collection_crud: collection = await collection_crud.get_collection(id) - print(collection) + if collection is None: + raise ValueError if collection.user != user_id: raise NotAuthorizedError return collection diff --git a/linguaphoto/api/image.py b/linguaphoto/api/image.py index 8884289..d912370 100644 --- a/linguaphoto/api/image.py +++ b/linguaphoto/api/image.py @@ -54,8 +54,9 @@ async def delete_image( if image: async with collection_crud: collection = await collection_crud.get_collection(image.collection) - updated_images = list(filter(lambda image: image != id, collection.images)) - await collection_crud.edit_collection(image.collection, {"images": updated_images}) + if collection: + updated_images = list(filter(lambda image: image != id, collection.images)) + await collection_crud.edit_collection(image.collection, {"images": updated_images}) await image_crud.delete_image(id) return raise HTTPException(status_code=400, detail="Image is invalid") diff --git a/linguaphoto/api/user.py b/linguaphoto/api/user.py index 2d6ba55..02a03ae 100644 --- a/linguaphoto/api/user.py +++ b/linguaphoto/api/user.py @@ -10,7 +10,11 @@ UserSigninRespondFragment, UserSignupFragment, ) -from linguaphoto.utils.auth import create_access_token, decode_access_token, oauth2_schema +from linguaphoto.utils.auth import ( + create_access_token, + decode_access_token, + oauth2_schema, +) router = APIRouter() diff --git a/linguaphoto/crud/base.py b/linguaphoto/crud/base.py index d298c11..4695614 100644 --- a/linguaphoto/crud/base.py +++ b/linguaphoto/crud/base.py @@ -170,7 +170,7 @@ async def _update_item( raise ValueError(f"Invalid update: {str(e)}") raise - async def _delete_item(self, item: BaseModel | str) -> None: + async def _delete_item(self, item: LinguaBaseModel | str) -> None: table = await self.db.Table(TABLE_NAME) await table.delete_item(Key={"id": item if isinstance(item, str) else item.id}) diff --git a/linguaphoto/crud/collection.py b/linguaphoto/crud/collection.py index 631287a..32e11e2 100644 --- a/linguaphoto/crud/collection.py +++ b/linguaphoto/crud/collection.py @@ -12,7 +12,7 @@ async def create_collection(self, user_id: str, title: str, description: str) -> await self._add_item(collection) return collection - async def get_collection(self, collection_id: str) -> Collection: + async def get_collection(self, collection_id: str) -> Collection | None: collection = await self._get_item(collection_id, Collection, True) return collection diff --git a/linguaphoto/crud/image.py b/linguaphoto/crud/image.py index 61d7837..f126908 100644 --- a/linguaphoto/crud/image.py +++ b/linguaphoto/crud/image.py @@ -73,7 +73,7 @@ async def get_images(self, collection_id: str, user_id: str) -> List[Image]: images = await self._get_items_from_secondary_index("user", user_id, Image, Key("collection").eq(collection_id)) return images - async def get_image(self, image_id: str) -> Image: + async def get_image(self, image_id: str) -> Image | None: image = await self._get_item(image_id, Image, True) return image @@ -86,6 +86,8 @@ async def translate(self, images: List[str], user_id: str) -> List[Image]: for id in images: # Retrieve image metadata and download the image content image_instance = await self._get_item(id, Image, True) + if image_instance is None: + continue response = requests.get(image_instance.image_url) if response.status_code == 200: img_source = BytesIO(response.content) @@ -102,6 +104,8 @@ async def translate(self, images: List[str], user_id: str) -> List[Image]: # Set buffer position to the start audio_buffer.seek(0) audio_url = await self.create_audio(audio_buffer) + if audio_url is None: + continue # Attach the audio URL to the transcription transcription.audio_url = audio_url image_instance.transcriptions = transcription_response.transcriptions diff --git a/linguaphoto/crud/user.py b/linguaphoto/crud/user.py index b7edefb..f7a3e11 100644 --- a/linguaphoto/crud/user.py +++ b/linguaphoto/crud/user.py @@ -32,6 +32,5 @@ async def verify_user_by_email(self, user: UserSigninFragment) -> bool: else: raise ValueError - async def update_user(self, id: str, data: dict) -> User | None: - user = await self._update_item(id, User, data) - return user + async def update_user(self, id: str, data: dict) -> None: + await self._update_item(id, User, data) diff --git a/linguaphoto/main.py b/linguaphoto/main.py index 4c4f21f..b92c07a 100644 --- a/linguaphoto/main.py +++ b/linguaphoto/main.py @@ -5,7 +5,6 @@ from fastapi.middleware.cors import CORSMiddleware from linguaphoto.api.api import router -from linguaphoto.settings import settings app = FastAPI() @@ -18,94 +17,8 @@ allow_headers=["*"], ) -# Retrieve AWS configuration from environment variables -bucket_name = settings.bucket_name -dynamodb_table_name = settings.dynamodb_table_name -media_hosting_server = settings.media_hosting_server -key_pair_id = settings.key_pair_id - app.include_router(router, prefix="") - -# class ImageMetadata(BaseModel): -# filename: str -# s3_url: str - - -# @app.post("/upload/", response_model=ImageMetadata) -# async def upload_image(file: UploadFile = File(...)) -> ImageMetadata: -# if file.filename is None or not file.filename: -# raise HTTPException(status_code=400, detail="File name is missing.") - -# try: -# # Generate a unique file name -# file_extension = file.filename.split(".")[-1] if "." in file.filename else "unknown" -# unique_filename = f"{uuid.uuid4()}.{file_extension}" - -# if bucket_name is None: -# raise HTTPException(status_code=500, detail="Bucket name is not set.") - -# if dynamodb_table_name is None: -# raise HTTPException(status_code=500, detail="DynamoDB table name is not set.") - -# # Create an instance of CloudFrontUrlSigner -# private_key_path = os.path.abspath("private_key.pem") -# cfs = CloudFrontUrlSigner(str(key_pair_id), private_key_path) -# # Generate a signed URL -# url = f"{media_hosting_server}/{unique_filename}" -# custom_policy = cfs.create_custom_policy(url, expire_days=100) -# s3_url = cfs.generate_presigned_url(url, custom_policy) -# print(s3_url) -# # Create an S3 client with aioboto3 -# async with aioboto3.Session().client( -# "s3", -# region_name=settings.aws_region_name, -# aws_access_key_id=settings.aws_access_key_id, -# aws_secret_access_key=settings.aws_secret_access_key, -# ) as s3_client: -# # Upload the file to S3 -# await s3_client.upload_fileobj(file.file, bucket_name, f"uploads/{unique_filename}") - -# # Create a DynamoDB resource with aioboto3 -# async with aioboto3.Session().resource( -# "dynamodb", -# region_name=settings.aws_region_name, -# aws_access_key_id=settings.aws_access_key_id, -# aws_secret_access_key=settings.aws_secret_access_key, -# ) as dynamodb: -# table = await dynamodb.Table(dynamodb_table_name) -# # Save metadata to DynamoDB -# await table.put_item(Item={"id": unique_filename, "s3_url": s3_url}) - -# return ImageMetadata(filename=unique_filename, s3_url=s3_url) - -# except Exception as e: -# print(str(e)) -# raise HTTPException(status_code=500, detail=str(e)) - - -# @app.get("/download/{filename}") -# async def download_image(filename: str): -# try: -# async with aioboto3.Session().resource( -# "dynamodb", -# region_name=settings.aws_region_name, -# aws_access_key_id=settings.aws_access_key_id, -# aws_secret_access_key=settings.aws_secret_access_key, -# ) as dynamodb: -# table = await dynamodb.Table(dynamodb_table_name) -# # Retrieve image metadata from DynamoDB -# response = await table.get_item(Key={"id": filename}) - -# if "Item" not in response: -# raise HTTPException(status_code=404, detail="Image not found") - -# # Return the S3 URL for download -# return {"s3_url": response["Item"]["s3_url"]} - -# except Exception as e: -# raise HTTPException(status_code=500, detail=str(e)) - if __name__ == "__main__": print("Starting webserver...") uvicorn.run(app, port=8080, host="0.0.0.0") diff --git a/linguaphoto/models.py b/linguaphoto/models.py index 4905f06..15baa57 100644 --- a/linguaphoto/models.py +++ b/linguaphoto/models.py @@ -79,7 +79,7 @@ class TranscriptionResponse(BaseModel): class Image(LinguaBaseModel): is_translated: bool = False transcriptions: list[Transcription] = [] - collection: str | None = None + collection: str image_url: str user: str diff --git a/linguaphoto/settings.py b/linguaphoto/settings.py index 4aab1d2..f775b3c 100644 --- a/linguaphoto/settings.py +++ b/linguaphoto/settings.py @@ -18,7 +18,7 @@ class Settings: - bucket_name = os.getenv("S3_BUCKET_NAME") + bucket_name = os.getenv("S3_BUCKET_NAME", "linguaphoto") dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME", "linguaphoto") media_hosting_server = os.getenv("MEDIA_HOSTING_SERVER") key_pair_id = os.getenv("KEY_PAIR_ID") @@ -27,7 +27,7 @@ class Settings: aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") openai_key = os.getenv("OPENAI_API_KEY") stripe_key = os.getenv("STRIPE_API_KEY") - stripe_price_id = os.getenv("STRIPE_PRODUCT_PRICE_ID") + stripe_price_id = os.getenv("STRIPE_PRODUCT_PRICE_ID", "price_1Q0ZaMKeTo38dsfeSWRDGCEf") settings = Settings() diff --git a/linguaphoto/utils/auth.py b/linguaphoto/utils/auth.py index 97fc31a..108f37e 100644 --- a/linguaphoto/utils/auth.py +++ b/linguaphoto/utils/auth.py @@ -61,6 +61,8 @@ async def subscription_validate(token: str = Depends(oauth2_schema), user_crud: raise HTTPException(status_code=422, detail="Could not validate credentials") async with user_crud: user = await user_crud.get_user(user_id, True) + if user is None: + raise HTTPException(status_code=422, detail="Could not validate credentials") if user.is_subscription is False: raise HTTPException(status_code=422, detail="You need to subscribe.") return True diff --git a/linguaphoto/utils/cloudfront_url_signer.py b/linguaphoto/utils/cloudfront_url_signer.py index 07ff904..3697dcc 100644 --- a/linguaphoto/utils/cloudfront_url_signer.py +++ b/linguaphoto/utils/cloudfront_url_signer.py @@ -33,7 +33,7 @@ def _rsa_signer(self, message: str) -> bytes: with open(self.private_key_path, "r") as key_file: private_key = key_file.read() return rsa.sign( - message, # Ensure message is in bytes + message.encode("utf8"), # Ensure message is in bytes rsa.PrivateKey.load_pkcs1(private_key.encode("utf8")), "SHA-1", # CloudFront requires SHA-1 hash ) diff --git a/linguaphoto/private_key.pem b/private_key.pem similarity index 100% rename from linguaphoto/private_key.pem rename to private_key.pem From f2e4e0a099ca96e9f983ca238cf286b5cdd8d729 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Sep 2024 12:34:55 +0300 Subject: [PATCH 02/13] feature_db_py --- apprunner.yaml | 2 + debug.py | 1 + linguaphoto/crud/base.py | 116 ++++++++++++++++++++++++++++++++++++- linguaphoto/db.py | 110 +++++++++++++++++++++++++++++++++++ linguaphoto/settings.py | 1 + linguaphoto/utils/utils.py | 12 ++++ 6 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 linguaphoto/db.py create mode 100644 linguaphoto/utils/utils.py diff --git a/apprunner.yaml b/apprunner.yaml index 0fd0c55..99c12f3 100644 --- a/apprunner.yaml +++ b/apprunner.yaml @@ -14,6 +14,8 @@ build: run: command: venv/bin/python3 linguaphoto/main.py # Adjust to the path to your application entry point env: + - name: HOMEPAGE_URL + value: "linguaphoto.com" - name: DYNAMODB_TABLE_NAME value: "linguaphoto" - name: S3_BUCKET_NAME diff --git a/debug.py b/debug.py index c404722..0b59d80 100644 --- a/debug.py +++ b/debug.py @@ -1,4 +1,5 @@ """it is entity for debugging""" + import uvicorn from linguaphoto.main import app diff --git a/linguaphoto/crud/base.py b/linguaphoto/crud/base.py index 4695614..c3cbdf0 100644 --- a/linguaphoto/crud/base.py +++ b/linguaphoto/crud/base.py @@ -1,8 +1,9 @@ """Defines the base CRUD interface.""" +import itertools import logging from types import TracebackType -from typing import Any, AsyncContextManager, BinaryIO, Self, TypeVar +from typing import Any, AsyncContextManager, BinaryIO, Literal, Self, TypeVar import aioboto3 from boto3.dynamodb.conditions import ComparisonCondition, Key @@ -13,6 +14,7 @@ from linguaphoto.errors import InternalError, ItemNotFoundError from linguaphoto.models import BaseModel, LinguaBaseModel from linguaphoto.settings import settings +from linguaphoto.utils.utils import get_cors_origins T = TypeVar("T", bound=BaseModel) @@ -21,6 +23,9 @@ DEFAULT_SCAN_LIMIT = 1000 ITEMS_PER_PAGE = 12 +TableKey = tuple[str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]] +GlobalSecondaryIndex = tuple[str, str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]] + logger = logging.getLogger(__name__) @@ -45,6 +50,10 @@ def s3(self) -> S3ServiceResource: def get_gsi_index_name(cls, colname: str) -> str: return f"{colname}-index" + @classmethod + def get_gsis(cls) -> set[str]: + return {"type"} + async def __aenter__(self) -> Self: session = aioboto3.Session() db = await session.resource( @@ -174,6 +183,66 @@ async def _delete_item(self, item: LinguaBaseModel | str) -> None: table = await self.db.Table(TABLE_NAME) await table.delete_item(Key={"id": item if isinstance(item, str) else item.id}) + async def _create_dynamodb_table( + self, + name: str, + keys: list[TableKey], + gsis: list[GlobalSecondaryIndex] | None = None, + deletion_protection: bool = False, + ) -> None: + """Creates a table in the Dynamo database if a table of that name does not already exist. + + Args: + name: Name of the table. + keys: Primary and secondary keys. Do not include non-key attributes. + gsis: Making an attribute a GSI is required in order to query + against it. Note HASH on a GSI does not actually enforce + uniqueness. Instead, the difference is: you cannot query + RANGE fields alone, but you may query HASH fields. + deletion_protection: Whether the table is protected from being + deleted. + """ + try: + await self.db.meta.client.describe_table(TableName=name) + logger.info("Found existing table %s", name) + except ClientError: + logger.info("Creating %s table", name) + + if gsis: + table = await self.db.create_table( + AttributeDefinitions=[ + {"AttributeName": n, "AttributeType": t} + for n, t in itertools.chain(((n, t) for (n, t, _) in keys), ((n, t) for _, n, t, _ in gsis)) + ], + TableName=name, + KeySchema=[{"AttributeName": n, "KeyType": t} for n, _, t in keys], + GlobalSecondaryIndexes=( + [ + { + "IndexName": i, + "KeySchema": [{"AttributeName": n, "KeyType": t}], + "Projection": {"ProjectionType": "ALL"}, + } + for i, n, _, t in gsis + ] + ), + DeletionProtectionEnabled=deletion_protection, + BillingMode="PAY_PER_REQUEST", + ) + + else: + table = await self.db.create_table( + AttributeDefinitions=[ + {"AttributeName": n, "AttributeType": t} for n, t in ((n, t) for (n, t, _) in keys) + ], + TableName=name, + KeySchema=[{"AttributeName": n, "KeyType": t} for n, _, t in keys], + DeletionProtectionEnabled=deletion_protection, + BillingMode="PAY_PER_REQUEST", + ) + + await table.wait_until_exists() + async def _list_items( self, item_class: type[T], @@ -206,3 +275,48 @@ async def _list_items( async def _upload_to_s3(self, file: BinaryIO, unique_filename: str) -> None: bucket = await self.s3.Bucket(settings.bucket_name) await bucket.upload_fileobj(file, f"uploads/{unique_filename}") + + async def _create_s3_bucket(self) -> None: + """Creates an S3 bucket if it does not already exist.""" + try: + await self.s3.meta.client.head_bucket(Bucket=settings.bucket_name) + logger.info("Found existing bucket %s", settings.bucket_name) + except ClientError: + logger.info("Creating %s bucket", settings.bucket_name) + await self.s3.create_bucket(Bucket=settings.bucket_name) + + logger.info("Updating %s CORS configuration", settings.bucket_name) + s3_cors = await self.s3.BucketCors(settings.bucket_name) + await s3_cors.put( + CORSConfiguration={ + "CORSRules": [ + { + "AllowedHeaders": ["*"], + "AllowedMethods": ["GET"], + "AllowedOrigins": get_cors_origins(), + "ExposeHeaders": ["ETag"], + } + ] + }, + ) + + async def _delete_dynamodb_table(self, name: str) -> None: + """Deletes a table in the Dynamo database. + + Args: + name: Name of the table. + """ + try: + table = await self.db.Table(name) + await table.delete() + logger.info("Deleted table %s", name) + except ClientError: + logger.info("Table %s does not exist", name) + + async def _delete_s3_bucket(self) -> None: + """Deletes an S3 bucket.""" + bucket = await self.s3.Bucket(settings.bucket_name) + logger.info("Deleting bucket %s", settings.bucket_name) + async for obj in bucket.objects.all(): + await obj.delete() + await bucket.delete() diff --git a/linguaphoto/db.py b/linguaphoto/db.py new file mode 100644 index 0000000..c354baf --- /dev/null +++ b/linguaphoto/db.py @@ -0,0 +1,110 @@ +"""Defines base tools for interacting with the database.""" + +import argparse +import asyncio +import logging +from typing import AsyncGenerator, Literal, Self + +from linguaphoto.crud.base import TABLE_NAME, BaseCrud +from linguaphoto.crud.collection import CollectionCrud +from linguaphoto.crud.image import ImageCrud +from linguaphoto.crud.user import UserCrud + + +class Crud( + CollectionCrud, + ImageCrud, + UserCrud, + BaseCrud, +): + """Composes the various CRUD classes into a single class.""" + + @classmethod + async def get(cls) -> AsyncGenerator[Self, None]: + async with cls() as crud: + yield crud + + +async def create_tables(crud: Crud | None = None, deletion_protection: bool = False) -> None: + """Initializes all of the database tables. + + Args: + crud: The top-level CRUD class. + deletion_protection: Whether to enable deletion protection on the tables. + """ + logging.basicConfig(level=logging.INFO) + + if crud is None: + async with Crud() as new_crud: + await create_tables(new_crud) + + else: + gsis_set = crud.get_gsis() + gsis: list[tuple[str, str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]]] = [ + (Crud.get_gsi_index_name(g), g, "S", "HASH") for g in gsis_set + ] + + await asyncio.gather( + crud._create_dynamodb_table( + name=TABLE_NAME, + keys=[ + ("id", "S", "HASH"), + ], + gsis=gsis, + deletion_protection=deletion_protection, + ), + crud._create_s3_bucket(), + ) + + +async def delete_tables(crud: Crud | None = None) -> None: + """Deletes all of the database tables. + + Args: + crud: The top-level CRUD class. + """ + logging.basicConfig(level=logging.INFO) + + if crud is None: + async with Crud() as new_crud: + await delete_tables(new_crud) + + else: + await crud._delete_dynamodb_table(TABLE_NAME) + await crud._delete_s3_bucket() + + +async def populate_with_dummy_data(crud: Crud | None = None) -> None: + """Populates the database with dummy data. + + Args: + crud: The top-level CRUD class. + """ + if crud is None: + async with Crud() as new_crud: + await populate_with_dummy_data(new_crud) + + else: + raise NotImplementedError("This function is not yet implemented.") + + +async def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["create", "delete", "populate"]) + args = parser.parse_args() + + async with Crud() as crud: + match args.action: + case "create": + await create_tables(crud) + case "delete": + await delete_tables(crud) + case "populate": + await populate_with_dummy_data(crud) + case _: + raise ValueError(f"Invalid action: {args.action}") + + +if __name__ == "__main__": + # python -m store.app.db + asyncio.run(main()) diff --git a/linguaphoto/settings.py b/linguaphoto/settings.py index f775b3c..6e4fd95 100644 --- a/linguaphoto/settings.py +++ b/linguaphoto/settings.py @@ -28,6 +28,7 @@ class Settings: openai_key = os.getenv("OPENAI_API_KEY") stripe_key = os.getenv("STRIPE_API_KEY") stripe_price_id = os.getenv("STRIPE_PRODUCT_PRICE_ID", "price_1Q0ZaMKeTo38dsfeSWRDGCEf") + homepage_url = os.getenv("HOMEPAGE_URL", "") settings = Settings() diff --git a/linguaphoto/utils/utils.py b/linguaphoto/utils/utils.py new file mode 100644 index 0000000..9b9f65c --- /dev/null +++ b/linguaphoto/utils/utils.py @@ -0,0 +1,12 @@ +"""Defines package-wide utility functions.""" + +from linguaphoto.settings import settings + +LOCALHOST_URLS = [ + "http://127.0.0.1:3000", + "http://localhost:3000", +] + + +def get_cors_origins() -> list[str]: + return list({settings.homepage_url, *LOCALHOST_URLS}) From 5027d2f00940cfe0863a07a9fe7a43802fb8a794 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Sep 2024 17:00:12 +0300 Subject: [PATCH 03/13] fix_audio_issue --- frontend/src/components/Audio.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/Audio.tsx b/frontend/src/components/Audio.tsx index d48e484..5dea933 100644 --- a/frontend/src/components/Audio.tsx +++ b/frontend/src/components/Audio.tsx @@ -93,6 +93,7 @@ const AudioPlayer: React.FC = ({ currentImage, index }) => { useEffect(() => { if (audioRef.current) { audioRef.current.load(); + audioRef.current.playbackRate = playbackRate; } }, [currentImage, index]); From ea27e2d21d5d8dd32134b3c042074897a0ef7c3e Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Sep 2024 17:12:31 +0300 Subject: [PATCH 04/13] chores_lint_error --- linguaphoto/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/linguaphoto/settings.py b/linguaphoto/settings.py index e5a5b8e..6e4fd95 100644 --- a/linguaphoto/settings.py +++ b/linguaphoto/settings.py @@ -31,5 +31,4 @@ class Settings: homepage_url = os.getenv("HOMEPAGE_URL", "") - settings = Settings() From 8c7d536fe48298a24e20ff0ca5b6cf10a915759e Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 2 Oct 2024 19:57:11 +0300 Subject: [PATCH 05/13] fix_openAPI_switching --- .gitignore | 2 +- frontend/src/App.tsx | 2 - frontend/src/api/api.ts | 8 +- frontend/src/contexts/AuthContext.tsx | 98 +- frontend/src/contexts/api.tsx | 28 + frontend/src/gen/api.ts | 2935 ++++++++++++++++++++ frontend/src/pages/Collection.tsx | 51 +- frontend/src/pages/Collections.tsx | 44 +- frontend/src/pages/Login.tsx | 36 +- frontend/src/pages/SubscriptioinType.tsx | 31 +- frontend/src/pages/Test.tsx | 117 - linguaphoto/api/image.py | 10 +- linguaphoto/api/subscription.py | 5 +- linguaphoto/private_key.pem | 27 + linguaphoto/utils/cloudfront_url_signer.py | 4 +- 15 files changed, 3161 insertions(+), 237 deletions(-) create mode 100644 frontend/src/contexts/api.tsx create mode 100644 frontend/src/gen/api.ts delete mode 100644 frontend/src/pages/Test.tsx create mode 100644 linguaphoto/private_key.pem diff --git a/.gitignore b/.gitignore index 706268f..85a8102 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,5 @@ dist/ out*/ venv/ - +.env linguaphoto/.env diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0baee0e..d5175f8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,7 +13,6 @@ import LoginPage from "pages/Login"; import NotFound from "pages/NotFound"; import SubscriptionTypePage from "pages/SubscriptioinType"; import SubscriptionCancelPage from "pages/Subscription"; -import Test from "pages/Test"; import PrivateRoute from "ProtectedRoute"; import { Container } from "react-bootstrap"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; @@ -30,7 +29,6 @@ const App = () => { } /> - } /> } /> ): Promise> { - const response = await this.api.post( - "/translate", - { images }, - { timeout: 300000 }, - ); + const response = await this.api.post("/translate", images, { + timeout: 300000, + }); return response.data; } diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 88dcabf..cdbd4ac 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,53 +1,111 @@ // src/context/AuthContext.tsx -import { read_me } from "api/auth"; +import { components, paths } from "gen/api"; +import createClient, { Client } from "openapi-fetch"; import React, { createContext, ReactNode, useContext, useEffect, + useMemo, useState, } from "react"; -import { Response } from "types/auth"; interface AuthContextType { - auth: Response | null; - setAuth: React.Dispatch>; + auth: components["schemas"]["UserInfoResponseItem"] | undefined; + setAuth: React.Dispatch< + React.SetStateAction< + components["schemas"]["UserInfoResponseItem"] | undefined + > + >; + setApiKeyId: React.Dispatch>; signout: () => void; + apiKeyId: string | null; + client: Client; } -const AuthContext = createContext(undefined); +const getLocalStorageAuth = (): string | null => { + return localStorage.getItem("token"); +}; + +export const setLocalStorageAuth = (id: string) => { + localStorage.setItem("token", id); +}; + +export const deleteLocalStorageAuth = () => { + localStorage.removeItem("token"); +}; +const AuthContext = createContext(undefined); const AuthProvider = ({ children }: { children: ReactNode }) => { - const [auth, setAuth] = useState(null); + const [auth, setAuth] = useState< + components["schemas"]["UserInfoResponseItem"] | undefined + >(undefined); + const [apiKeyId, setApiKeyId] = useState( + getLocalStorageAuth(), + ); const signout = () => { localStorage.removeItem("token"); - setAuth({}); + setAuth(undefined); + setApiKeyId(""); }; + const client = useMemo( + () => + createClient({ + baseUrl: process.env.REACT_APP_BACKEND_URL, + }), + [apiKeyId], + ); useEffect(() => { - const token = localStorage.getItem("token"); - if (token) { - const fetch_data = async (token: string) => { - try { - const response = await read_me(token); - setAuth(response); - } catch { - return; + if (apiKeyId !== null) { + setLocalStorageAuth(apiKeyId); + client.use({ + async onRequest({ request }) { + request.headers.set("Authorization", `Bearer ${apiKeyId}`); + return request; + }, + async onResponse({ response }) { + return response; + }, + }); + } + }, [apiKeyId, client]); + useEffect(() => { + if (apiKeyId) { + const fetch_data = async () => { + const { data, error } = await client.GET("/me"); + if (error) { + console.error("Failed to fetch current user", error); + } else { + setAuth(data); + setApiKeyId(data.token); } }; - fetch_data(token); + fetch_data(); } else signout(); - }, []); + }, [apiKeyId, client]); + useEffect(() => { - if (auth?.token) { - localStorage.setItem("token", auth.token); + if (apiKeyId !== null) { + client.use({ + async onRequest({ request }) { + request.headers.set("Authorization", `Bearer ${apiKeyId}`); + return request; + }, + async onResponse({ response }) { + return response; + }, + }); } - }, [auth?.token]); + }, [apiKeyId, client]); return ( {children} diff --git a/frontend/src/contexts/api.tsx b/frontend/src/contexts/api.tsx new file mode 100644 index 0000000..5385fcf --- /dev/null +++ b/frontend/src/contexts/api.tsx @@ -0,0 +1,28 @@ +import type { paths } from "gen/api"; +import { Client } from "openapi-fetch"; + +export default class api { + public client: Client; + + constructor(client: Client) { + this.client = client; + } + + public async upload(files: File[], listing_id: string) { + return await this.client.POST("/artifacts/upload/{listing_id}", { + body: { + files: [], + }, + params: { + path: { + listing_id, + }, + }, + bodySerializer() { + const fd = new FormData(); + files.forEach((file) => fd.append("files", file)); + return fd; + }, + }); + } +} diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts new file mode 100644 index 0000000..d865f9f --- /dev/null +++ b/frontend/src/gen/api.ts @@ -0,0 +1,2935 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ +import { Collection, Image } from "types/model"; +export interface paths { + "/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Root */ + get: operations["read_root__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/url/{artifact_type}/{listing_id}/{name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Artifact Url */ + get: operations["artifact_url_artifacts_url__artifact_type___listing_id___name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/info/{artifact_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Artifact Info */ + get: operations["get_artifact_info_artifacts_info__artifact_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/list/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Artifacts */ + get: operations["list_artifacts_artifacts_list__listing_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/upload/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Upload */ + post: operations["upload_artifacts_upload__listing_id__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/edit/{artifact_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Edit Artifact */ + put: operations["edit_artifact_artifacts_edit__artifact_id__put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/delete/{artifact_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Artifact */ + delete: operations["delete_artifact_artifacts_delete__artifact_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/translate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["translate"]; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/get_images": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get_images"]; + put?: never; + post?: never; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/delete_image": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["delete_image"]; + put?: never; + post?: never; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/create_collection": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["create_collection"]; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/edit_collection": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["edit_collection"]; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/get_collection": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get_collection"]; + put?: never; + post?: never; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/get_collections": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get_collections"]; + put?: never; + post?: never; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/delete_collection": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["delete_collections"]; + put?: never; + post?: never; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/create_subscription": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["create_subscription"]; + /** Delete Artifact */ + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email/signup/create": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Signup Token + * @description Creates a signup token and emails it to the user. + */ + post: operations["create_signup_token_email_signup_create_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email/signup/get/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Signup Token */ + get: operations["get_signup_token_email_signup_get__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email/signup/delete/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Signup Token */ + delete: operations["delete_signup_token_email_signup_delete__id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/keys/new": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** New Key */ + post: operations["new_key_keys_new_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/keys/list": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Keys */ + get: operations["list_keys_keys_list_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/keys/delete/{key}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Key */ + delete: operations["delete_key_keys_delete__key__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Listings */ + get: operations["list_listings_listings_search_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Batch Listing Info */ + get: operations["get_batch_listing_info_listings_batch_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/dump": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Dump Listings */ + get: operations["dump_listings_listings_dump_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/user/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List User Listings */ + get: operations["list_user_listings_listings_user__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List My Listings */ + get: operations["list_my_listings_listings_me_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/add": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Add Listing */ + post: operations["add_listing_listings_add_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/delete/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Listing */ + delete: operations["delete_listing_listings_delete__listing_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/edit/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Edit Listing */ + put: operations["edit_listing_listings_edit__id__put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Listing */ + get: operations["get_listing_listings__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/{id}/view": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Increment View Count */ + post: operations["increment_view_count_listings__id__view_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/{id}/vote": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Vote Listing */ + post: operations["vote_listing_listings__id__vote_post"]; + /** Remove Vote */ + delete: operations["remove_vote_listings__id__vote_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/onshape/set/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set Onshape Document */ + post: operations["set_onshape_document_onshape_set__listing_id__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/onshape/pull/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Pull Onshape Document */ + get: operations["pull_onshape_document_onshape_pull__listing_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get User Info Endpoint */ + get: operations["get_user_info_endpoint_users_me_get"]; + /** Update Profile */ + put: operations["update_profile_users_me_put"]; + post?: never; + /** Delete User Endpoint */ + delete: operations["delete_user_endpoint_users_me_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Logout User Endpoint */ + delete: operations["logout_user_endpoint_users_logout_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/signup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Register User */ + post: operations["register_user_users_signup_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/signin": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Login User */ + post: operations["login_user_users_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Users Batch Endpoint */ + get: operations["get_users_batch_endpoint_users_batch_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/public/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Users Public Batch Endpoint */ + get: operations["get_users_public_batch_endpoint_users_public_batch_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get User Info By Id Endpoint */ + get: operations["get_user_info_by_id_endpoint_users__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/public/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get My Public User Info Endpoint */ + get: operations["get_my_public_user_info_endpoint_users_public_me_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/public/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Public User Info By Id Endpoint */ + get: operations["get_public_user_info_by_id_endpoint_users_public__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/validate-api-key": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Validate Api Key Endpoint */ + get: operations["validate_api_key_endpoint_users_validate_api_key_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/github/client-id": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Github Client Id Endpoint */ + get: operations["github_client_id_endpoint_users_github_client_id_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/github/code": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Github Code + * @description Gives the user a session token upon successful github authentication and creation of user. + * + * Args: + * data: The request body, containing the code from the OAuth redirect. + * crud: The CRUD object. + * response: The response object. + * + * Returns: + * UserInfoResponse. + */ + post: operations["github_code_users_github_code_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/google/client-id": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Google Client Id Endpoint */ + get: operations["google_client_id_endpoint_users_google_client_id_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/google/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Google Login Endpoint */ + post: operations["google_login_endpoint_users_google_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/set-moderator": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set Moderator */ + post: operations["set_moderator_users_set_moderator_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** ArtifactUrls */ + ArtifactUrls: { + /** Small */ + small?: string | null; + /** Large */ + large: string; + }; + /** AuthResponse */ + AuthResponse: { + /** Api Key */ + api_key: string; + }; + /** Body_pull_onshape_document_onshape_pull__listing_id__get */ + Body_pull_onshape_document_onshape_pull__listing_id__get: { + /** Suffix To Joint Effort */ + suffix_to_joint_effort?: { + [key: string]: number; + } | null; + /** Suffix To Joint Velocity */ + suffix_to_joint_velocity?: { + [key: string]: number; + } | null; + }; + /** Body_upload_artifacts_upload__listing_id__post */ + Body_upload_artifacts_upload__listing_id__post: { + /** Files */ + files: string[]; + }; + /** ClientIdResponse */ + ClientIdResponse: { + /** Client Id */ + client_id: string; + }; + /** DeleteTokenResponse */ + DeleteTokenResponse: { + /** Message */ + message: string; + }; + /** DumpListingsResponse */ + DumpListingsResponse: { + /** Listings */ + listings: components["schemas"]["Listing"][]; + }; + /** EmailSignUpRequest */ + EmailSignUpRequest: { + /** + * Email + * Format: email + */ + email: string; + }; + /** EmailSignUpResponse */ + EmailSignUpResponse: { + /** Message */ + message: string; + }; + + /** GetBatchListingsResponse */ + GetBatchListingsResponse: { + /** Listings */ + listings: components["schemas"]["ListingInfoResponse"][]; + }; + /** GetListingResponse */ + GetListingResponse: { + /** Id */ + id: string; + /** Name */ + name: string; + /** Description */ + description: string | null; + /** Child Ids */ + child_ids: string[]; + /** Tags */ + tags: string[]; + /** Onshape Url */ + onshape_url: string | null; + /** Can Edit */ + can_edit: boolean; + /** Created At */ + created_at: number; + /** Views */ + views: number; + /** Score */ + score: number; + /** User Vote */ + user_vote: boolean | null; + /** Creator Id */ + creator_id: string; + /** Creator Name */ + creator_name: string | null; + }; + /** GetTokenResponse */ + GetTokenResponse: { + /** Id */ + id: string; + /** Email */ + email: string; + }; + /** GithubAuthRequest */ + GithubAuthRequest: { + /** Code */ + code: string; + }; + + CollectionCreateRequest: { + title: string; + description: string; + }; + SubscriptionRequest: { + payment_method_id: string; + email: string; + name: string; + }; + SubscriptionResponse: { + success: boolean; + error: string; + }; + /** GithubAuthResponse */ + GithubAuthResponse: { + /** Api Key */ + api_key: string; + }; + /** GoogleLogin */ + GoogleLogin: { + /** Token */ + token: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** KeysResponseItem */ + KeysResponseItem: { + /** Token */ + token: string; + /** Permissions */ + permissions: ("read" | "write" | "admin")[] | null; + }; + /** ListArtifactsResponse */ + ListArtifactsResponse: { + /** Artifacts */ + artifacts: components["schemas"]["SingleArtifactResponse"][]; + }; + /** ListKeysResponse */ + ListKeysResponse: { + /** Keys */ + keys: components["schemas"]["KeysResponseItem"][]; + }; + /** ListListingsResponse */ + ListListingsResponse: { + /** Listing Ids */ + listing_ids: string[]; + /** + * Has Next + * @default false + */ + has_next: boolean; + }; + /** + * Listing + * @description Defines a recursively-defined listing. + * + * Listings can have sub-listings with their component parts. They can also + * have associated user-uploaded artifacts like images and URDFs. + */ + Listing: { + /** Id */ + id: string; + /** User Id */ + user_id: string; + /** Created At */ + created_at: number; + /** Updated At */ + updated_at: number; + /** Name */ + name: string; + /** Child Ids */ + child_ids: string[]; + /** Description */ + description?: string | null; + /** Onshape Url */ + onshape_url?: string | null; + /** + * Views + * @default 0 + */ + views: number; + /** + * Upvotes + * @default 0 + */ + upvotes: number; + /** + * Downvotes + * @default 0 + */ + downvotes: number; + /** + * Score + * @default 0 + */ + score: number; + }; + /** ListingInfoResponse */ + ListingInfoResponse: { + /** Id */ + id: string; + /** Name */ + name: string; + /** Description */ + description: string | null; + /** Child Ids */ + child_ids: string[]; + /** Image Url */ + image_url: string | null; + /** Onshape Url */ + onshape_url: string | null; + /** Created At */ + created_at: number; + /** Views */ + views: number; + /** Score */ + score: number; + /** User Vote */ + user_vote: boolean | null; + }; + /** LoginRequest */ + LoginRequest: { + /** + * Email + * Format: email + */ + email: string; + /** Password */ + password: string; + }; + /** LoginResponse */ + LoginResponse: { + /** User Id */ + user_id: string; + /** Token */ + token: string; + }; + /** MyUserInfoResponse */ + MyUserInfoResponse: { + /** User Id */ + user_id: string; + /** Email */ + email: string; + /** Github Id */ + github_id: string | null; + /** Google Id */ + google_id: string | null; + /** Permissions */ + permissions: ("is_admin" | "is_mod")[] | null; + /** First Name */ + first_name: string | null; + /** Last Name */ + last_name: string | null; + /** Name */ + name: string | null; + /** Bio */ + bio: string | null; + }; + /** NewKeyRequest */ + NewKeyRequest: { + /** + * Readonly + * @default true + */ + readonly: boolean; + }; + /** NewKeyResponse */ + NewKeyResponse: { + /** User Id */ + user_id: string; + key: components["schemas"]["KeysResponseItem"]; + }; + /** NewListingRequest */ + NewListingRequest: { + /** Name */ + name: string; + /** Child Ids */ + child_ids: string[]; + /** Description */ + description: string | null; + }; + /** NewListingResponse */ + NewListingResponse: { + /** Listing Id */ + listing_id: string; + }; + /** PublicUserInfoResponseItem */ + PublicUserInfoResponseItem: { + /** Id */ + id: string; + /** Email */ + email: string; + /** Permissions */ + permissions?: ("is_admin" | "is_mod")[] | null; + /** Created At */ + created_at?: number | null; + /** Updated At */ + updated_at?: number | null; + /** First Name */ + first_name?: string | null; + /** Last Name */ + last_name?: string | null; + /** Name */ + name?: string | null; + /** Bio */ + bio?: string | null; + }; + /** PublicUsersInfoResponse */ + PublicUsersInfoResponse: { + /** Users */ + users: components["schemas"]["PublicUserInfoResponseItem"][]; + }; + /** SetModeratorRequest */ + SetModeratorRequest: { + /** User Id */ + user_id: string; + /** Is Mod */ + is_mod: boolean; + }; + /** SetRequest */ + SetRequest: { + /** Onshape Url */ + onshape_url: string | null; + }; + /** SingleArtifactResponse */ + SingleArtifactResponse: { + /** Artifact Id */ + artifact_id: string; + /** Listing Id */ + listing_id: string; + /** Name */ + name: string; + /** Artifact Type */ + artifact_type: + | "image" + | ("urdf" | "mjcf") + | ("stl" | "obj" | "dae" | "ply") + | ("tgz" | "zip"); + /** Description */ + description: string | null; + /** Timestamp */ + timestamp: number; + urls: components["schemas"]["ArtifactUrls"]; + }; + /** + * SortOption + * @enum {string} + */ + SortOption: "newest" | "most_viewed" | "most_upvoted"; + /** UpdateArtifactRequest */ + UpdateArtifactRequest: { + /** Name */ + name?: string | null; + /** Description */ + description?: string | null; + }; + /** UpdateListingRequest */ + UpdateListingRequest: { + /** Name */ + name?: string | null; + /** Child Ids */ + child_ids?: string[] | null; + /** Description */ + description?: string | null; + /** Tags */ + tags?: string[] | null; + }; + /** UpdateUserRequest */ + UpdateUserRequest: { + /** Email */ + email?: string | null; + /** Password */ + password?: string | null; + /** Github Id */ + github_id?: string | null; + /** Google Id */ + google_id?: string | null; + /** First Name */ + first_name?: string | null; + /** Last Name */ + last_name?: string | null; + /** Name */ + name?: string | null; + /** Bio */ + bio?: string | null; + }; + /** UploadArtifactResponse */ + UploadArtifactResponse: { + /** Artifacts */ + artifacts: components["schemas"]["SingleArtifactResponse"][]; + }; + /** UserInfoResponseItem */ + UserInfoResponseItem: { + token: string; + username: string; + email: string; + is_subscription: boolean; + is_auth: boolean; + }; + /** + * UserPublic + * @description Defines public user model for frontend. + * + * Omits private/sesnsitive user fields. Is the return type for + * retrieving user data on frontend (for public profile pages, etc). + */ + UserPublic: { + /** Id */ + id: string; + /** Email */ + email: string; + /** Permissions */ + permissions?: ("is_admin" | "is_mod")[] | null; + /** Created At */ + created_at: number; + /** Updated At */ + updated_at?: number | null; + /** First Name */ + first_name?: string | null; + /** Last Name */ + last_name?: string | null; + /** Name */ + name?: string | null; + /** Bio */ + bio?: string | null; + }; + /** UserSignup */ + UserSignup: { + username: string; + /** Email */ + email: string; + /** Password */ + password: string; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + read_root__get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + artifact_url_artifacts_url__artifact_type___listing_id___name__get: { + parameters: { + query?: { + size?: "small" | "large"; + }; + header?: never; + path: { + artifact_type: + | "image" + | ("urdf" | "mjcf") + | ("stl" | "obj" | "dae" | "ply") + | ("tgz" | "zip"); + listing_id: string; + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_artifact_info_artifacts_info__artifact_id__get: { + parameters: { + query?: never; + header?: never; + path: { + artifact_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SingleArtifactResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_artifacts_artifacts_list__listing_id__get: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListArtifactsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + upload_artifacts_upload__listing_id__post: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_upload_artifacts_upload__listing_id__post"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadArtifactResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + edit_artifact_artifacts_edit__artifact_id__put: { + parameters: { + query: { + id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateArtifactRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_artifact_artifacts_delete__artifact_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + artifact_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_signup_token_email_signup_create_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EmailSignUpRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EmailSignUpResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + edit_collection: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: Collection; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": never; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_collection: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CollectionCreateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Collection; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_collection: { + parameters: { + query: { id: string }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Collection; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_collections: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Array; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_collections: { + parameters: { + query: { id: string }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": never; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + translate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": Array; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Array; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_images: { + parameters: { + query: { collection_id: string }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Array; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_image: { + parameters: { + query: { id: string }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": never; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + + create_subscription: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SubscriptionRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SubscriptionResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_signup_token_email_signup_get__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTokenResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_signup_token_email_signup_delete__id__delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteTokenResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + new_key_keys_new_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewKeyRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NewKeyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_keys_keys_list_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListKeysResponse"]; + }; + }; + }; + }; + delete_key_keys_delete__key__delete: { + parameters: { + query?: never; + header?: never; + path: { + key: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_listings_listings_search_get: { + parameters: { + query?: { + /** @description Page number for pagination */ + page?: number; + /** @description Search query string */ + search_query?: string; + /** @description Sort option for listings */ + sort_by?: components["schemas"]["SortOption"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_batch_listing_info_listings_batch_get: { + parameters: { + query: { + /** @description List of part ids */ + ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetBatchListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + dump_listings_listings_dump_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DumpListingsResponse"]; + }; + }; + }; + }; + list_user_listings_listings_user__id__get: { + parameters: { + query: { + /** @description Page number for pagination */ + page: number; + /** @description Search query string */ + search_query?: string; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_my_listings_listings_me_get: { + parameters: { + query: { + /** @description Page number for pagination */ + page: number; + /** @description Search query string */ + search_query?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_listing_listings_add_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewListingRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NewListingResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_listing_listings_delete__listing_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + edit_listing_listings_edit__id__put: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateListingRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_listing_listings__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetListingResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + increment_view_count_listings__id__view_post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + vote_listing_listings__id__vote_post: { + parameters: { + query: { + /** @description True for upvote, False for downvote */ + upvote: boolean; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_vote_listings__id__vote_delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_onshape_document_onshape_set__listing_id__post: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + pull_onshape_document_onshape_pull__listing_id__get: { + parameters: { + query?: { + token?: string | null; + default_prismatic_joint_effort?: number; + default_prismatic_joint_velocity?: number; + default_revolute_joint_effort?: number; + default_revolute_joint_velocity?: number; + voxel_size?: number; + convex_collision_meshes?: boolean; + add_mjcf?: boolean; + }; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Body_pull_onshape_document_onshape_pull__listing_id__get"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_user_info_endpoint_users_me_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponseItem"]; + }; + }; + }; + }; + update_profile_users_me_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateUserRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponseItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_user_endpoint_users_me_delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + logout_user_endpoint_users_logout_delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + register_user_users_signup_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UserSignup"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponseItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + login_user_users_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponseItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_users_batch_endpoint_users_batch_get: { + parameters: { + query: { + ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicUsersInfoResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_users_public_batch_endpoint_users_public_batch_get: { + parameters: { + query: { + ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicUsersInfoResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_user_info_by_id_endpoint_users__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponseItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_my_public_user_info_endpoint_users_public_me_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublic"]; + }; + }; + }; + }; + get_public_user_info_by_id_endpoint_users_public__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublic"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + validate_api_key_endpoint_users_validate_api_key_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + github_client_id_endpoint_users_github_client_id_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClientIdResponse"]; + }; + }; + }; + }; + github_code_users_github_code_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GithubAuthRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GithubAuthResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + google_client_id_endpoint_users_google_client_id_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClientIdResponse"]; + }; + }; + }; + }; + google_login_endpoint_users_google_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleLogin"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_moderator_users_set_moderator_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetModeratorRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublic"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/frontend/src/pages/Collection.tsx b/frontend/src/pages/Collection.tsx index 8ce541f..b77d9d7 100644 --- a/frontend/src/pages/Collection.tsx +++ b/frontend/src/pages/Collection.tsx @@ -29,12 +29,14 @@ const CollectionPage: React.FC = () => { const [currentImageIndex, setCurrentImageIndex] = useState(0); const [currentTranscriptionIndex, setCurrentTranscriptionIndex] = useState(0); const [currentImage, setCurrentImage] = useState(null); - const [collection, setCollection] = useState(null); - const { auth } = useAuth(); + const [collection, setCollection] = useState( + undefined, + ); + const { auth, client } = useAuth(); const { startLoading, stopLoading } = useLoading(); const [showUploadModal, setShowUploadModal] = useState(false); const [showDeleteImageModal, setShowDeleteImageModal] = useState(false); - const [images, setImages] = useState | null>([]); + const [images, setImages] = useState | undefined>([]); const [reorderImageIds, setReorderImageIds] = useState | null>( [], ); @@ -98,8 +100,12 @@ const CollectionPage: React.FC = () => { if (id && auth?.is_auth) { startLoading(); const asyncfunction = async () => { - const collection = await API.getCollection(id); - setCollection(collection); + const { data: collection, error } = await client.GET( + "/get_collection", + { params: { query: { id } } }, + ); + if (error) addAlert(error.detail?.toString(), "error"); + else setCollection(collection); stopLoading(); }; asyncfunction(); @@ -116,8 +122,11 @@ const CollectionPage: React.FC = () => { if (collection) { const asyncfunction = async () => { startLoading(); - const images = await API.getImages(collection.id); - setImages(images); + const { data: images, error } = await client.GET("/get_images", { + params: { query: { collection_id: collection.id } }, + }); + if (error) addAlert(error.detail?.toString(), "error"); + else setImages(images); stopLoading(); }; asyncfunction(); @@ -127,8 +136,12 @@ const CollectionPage: React.FC = () => { const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); startLoading(); - const collection = await API.createCollection({ title, description }); - if (collection != null) { + const { data: collection, error } = await client.POST( + "/create_collection", + { body: { title, description } }, + ); + if (error) addAlert(error.detail?.toString(), "error"); + else if (collection != null) { navigate(`/collection/${collection.id}?Action=edit`); addAlert("New collection has been created successfully!", "success"); } else addAlert("The process has gone wrong!", "error"); @@ -174,9 +187,12 @@ const CollectionPage: React.FC = () => { const asyncfunction = async () => { startLoading(); collection.images = reorderImageIds; - await API.editCollection(collection); - setCollection({ ...collection }); - addAlert("The collection has been updated successfully!", "success"); + const { error } = await client.POST("/edit_collection", { collection }); + if (error) addAlert(error.detail?.toString(), "error"); + else { + setCollection({ ...collection }); + addAlert("The collection has been updated successfully!", "success"); + } stopLoading(); }; asyncfunction(); @@ -188,7 +204,7 @@ const CollectionPage: React.FC = () => { const Image = await API_Uploader.uploadImage(file, collection?.id); stopLoading(); if (Image) { - const new_images: Array | null = images; + const new_images: Array | undefined = images; new_images?.push(Image); if (new_images != undefined) { setImages(new_images); @@ -231,8 +247,11 @@ const CollectionPage: React.FC = () => { const onDeleteImage = async () => { if (deleteImageId) { startLoading(); - await API.deleteImage(deleteImageId); - if (images) { + const { error } = await client.GET("/delete_image", { + params: { query: { id: deleteImageId } }, + }); + if (error) addAlert(error.detail?.toString(), "error"); + else if (images) { const filter = images.filter((image) => image.id !== deleteImageId); setImages(filter); const filteredId = collection?.images.filter( @@ -240,9 +259,9 @@ const CollectionPage: React.FC = () => { ); if (filteredId) setReorderImageIds(filteredId); else setReorderImageIds([]); + addAlert("The image has been deleted!", "success"); } setShowDeleteImageModal(false); - addAlert("The image has been deleted!", "success"); stopLoading(); } }; diff --git a/frontend/src/pages/Collections.tsx b/frontend/src/pages/Collections.tsx index 45af850..6570e2c 100644 --- a/frontend/src/pages/Collections.tsx +++ b/frontend/src/pages/Collections.tsx @@ -1,23 +1,20 @@ -import { Api } from "api/api"; -import axios, { AxiosInstance } from "axios"; import CardItem from "components/card"; import Modal from "components/modal"; import NewCardItem from "components/new_card"; import { useAuth } from "contexts/AuthContext"; import { useLoading } from "contexts/LoadingContext"; import { useAlertQueue } from "hooks/alerts"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Col, Row } from "react-bootstrap"; import { Collection } from "types/model"; const Collections = () => { const [collections, setCollection] = useState | []>([]); - const { auth } = useAuth(); + const { auth, client } = useAuth(); const [showModal, setShowModal] = useState(false); const { startLoading, stopLoading } = useLoading(); const [delete_ID, setDeleteID] = useState(String); const { addAlert } = useAlertQueue(); - const onDeleteModalShow = (id: string) => { setDeleteID(id); setShowModal(true); @@ -25,13 +22,18 @@ const Collections = () => { const onDelete = async () => { if (delete_ID) { startLoading(); - await API.deleteCollection(delete_ID); - const filter = collections?.filter( - (collection) => collection.id != delete_ID, - ); + const { error } = await client.GET("/delete_collection", { + params: { query: { id: delete_ID } }, + }); + if (error) addAlert(error.detail?.toString(), "error"); + else { + const filter = collections?.filter( + (collection) => collection.id != delete_ID, + ); + setCollection(filter); + addAlert("The Collection has been deleted.", "success"); + } setShowModal(false); - setCollection(filter); - addAlert("The Collection has been deleted.", "success"); stopLoading(); } }; @@ -40,28 +42,16 @@ const Collections = () => { if (auth?.is_auth) { const asyncfunction = async () => { startLoading(); - const collections = await API.getAllCollections(); - setCollection(collections); + const { data: collections, error } = + await client.GET("/get_collections"); + if (error) addAlert(error.detail?.toString(), "error"); + else setCollection(collections); stopLoading(); }; asyncfunction(); } }, [auth]); - const apiClient: AxiosInstance = useMemo( - () => - axios.create({ - baseURL: process.env.REACT_APP_BACKEND_URL, // Base URL for all requests - timeout: 10000, // Request timeout (in milliseconds) - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${auth?.token}`, // Add any default headers you need - }, - }), - [auth?.token], - ); - const API = useMemo(() => new Api(apiClient), [apiClient]); - return (

My Collections

diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 805ddf9..d74f61a 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,4 +1,3 @@ -import { signin, signup } from "api/auth"; import { useAuth } from "contexts/AuthContext"; import { useLoading } from "contexts/LoadingContext"; import { useAlertQueue } from "hooks/alerts"; @@ -11,7 +10,7 @@ const LoginPage: React.FC = () => { const [password, setPassword] = useState(""); const [username, setName] = useState(""); const { startLoading, stopLoading } = useLoading(); - const { auth, setAuth } = useAuth(); + const { auth, setAuth, client, setApiKeyId } = useAuth(); const navigate = useNavigate(); const { addAlert } = useAlertQueue(); useEffect(() => { @@ -29,24 +28,31 @@ const LoginPage: React.FC = () => { if (isSignup) { // You can call your API for sign-up startLoading(); - const user = await signup({ email, password, username }); - setAuth(user); - if (user) - addAlert("Welcome! You have been successfully signed up!", "success"); - else - addAlert( - "Sorry. The email or password have been exist already!", - "error", - ); + const { data, error } = await client.POST("/signup", { + body: { username, email, password }, + }); + if (error?.detail) addAlert(error.detail.toString(), "error"); + else { + if (data) { + setAuth(data); + setApiKeyId(data?.token); + addAlert("Welcome! You have been successfully signed up!", "success"); + } + } stopLoading(); } else { // You can call your API for login startLoading(); - const user = await signin({ email, password }); - setAuth(user); - if (user) + // const user = await signin({ email, password }); + const { data, error } = await client.POST("/signin", { + body: { email, password }, + }); + if (error) addAlert(error.detail?.toString(), "error"); + else { + setAuth(data); + setApiKeyId(data?.token); addAlert("Welcome! You have been successfully signed in!", "success"); - else addAlert("Sorry. The email or password are invalid.", "error"); + } stopLoading(); } }; diff --git a/frontend/src/pages/SubscriptioinType.tsx b/frontend/src/pages/SubscriptioinType.tsx index 1fb8a16..561300c 100644 --- a/frontend/src/pages/SubscriptioinType.tsx +++ b/frontend/src/pages/SubscriptioinType.tsx @@ -6,13 +6,11 @@ import { } from "@stripe/react-stripe-js"; import { loadStripe } from "@stripe/stripe-js"; -import { Api } from "api/api"; -import axios, { AxiosInstance } from "axios"; import { useAuth } from "contexts/AuthContext"; import { useLoading } from "contexts/LoadingContext"; import { useAlertQueue } from "hooks/alerts"; import { useTheme } from "hooks/theme"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_API_KEY || ""); @@ -22,29 +20,15 @@ const CheckoutForm = () => { const [error_message, setError] = useState(""); const [name, setName] = useState(""); // Cardholder Name const [email, setEmail] = useState(""); // Email Address - const { auth, setAuth } = useAuth(); + const { auth, setAuth, client } = useAuth(); const { theme } = useTheme(); const { addAlert } = useAlertQueue(); const { startLoading, stopLoading } = useLoading(); const navigate = useNavigate(); - const apiClient: AxiosInstance = useMemo( - () => - axios.create({ - baseURL: process.env.REACT_APP_BACKEND_URL, // Base URL for all requests - timeout: 10000, // Request timeout (in milliseconds) - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${auth?.token}`, // Add any default headers you need - }, - }), - [auth?.token], - ); useEffect(() => { if (auth?.email) setEmail(auth.email); }, [auth?.email]); - const API = useMemo(() => new Api(apiClient), [apiClient]); - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (elements == null || stripe == null) return; @@ -63,15 +47,18 @@ const CheckoutForm = () => { } startLoading(); // Send payment method to the backend for subscription creation - const data = await API.createSubscription(paymentMethod.id, email, name); + const { data, error: err } = await client.POST("/create_subscription", { + body: { payment_method_id: paymentMethod.id, email, name }, + }); stopLoading(); - if (data.success) { + if (err?.detail) addAlert(err.detail.toString(), "error"); + if (data?.success) { // Handle successful subscription (e.g., redirect or show success message) - setAuth({ ...auth, is_subscription: true }); + if (auth) setAuth({ ...auth, is_subscription: true }); addAlert("You have been subscribed successfully!", "success"); navigate("/collections"); } else { - setError(data.error); + setError(data?.error); } /* eslint-disable */ } catch (error: any) { /* eslint-enable */ diff --git a/frontend/src/pages/Test.tsx b/frontend/src/pages/Test.tsx deleted file mode 100644 index 5a970af..0000000 --- a/frontend/src/pages/Test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Api } from "api/api"; -import axios, { AxiosInstance } from "axios"; -import { ChangeEvent, useState } from "react"; -import { Col, Container, Row } from "react-bootstrap"; - -// Custom styles (you can include these in a separate CSS file or use styled-components) -const customStyles = { - input: { - marginBottom: "1rem", - borderRadius: "5px", - }, - button: { - backgroundColor: "#007bff", - borderColor: "#007bff", - color: "#fff", - borderRadius: "5px", - }, - img: { - maxWidth: "100%", - borderRadius: "10px", - boxShadow: "0 4px 8px rgba(0,0,0,0.3)", - }, - link: { - color: "#007bff", - textDecoration: "none", - }, -}; - -const Home = () => { - console.log(process.env.REACT_APP_BACKEND_URL); - const apiClient: AxiosInstance = axios.create({ - baseURL: process.env.REACT_APP_BACKEND_URL, // Base URL for all requests - // baseURL: 'https://api.linguaphoto.com', // Base URL for all requests - timeout: 10000, // Request timeout (in milliseconds) - headers: { - "Content-Type": "application/json", - Authorization: "Bearer your_token_here", // Add any default headers you need - }, - }); - const API = new Api(apiClient); - const [message, setMessage] = useState("Linguaphoto"); - const [imageURL, setImageURL] = useState(""); - const [file, setFile] = useState(null); - (async () => { - const text = await (async () => { - return await API.test(); - })(); - setMessage(text); - })(); - - const handleFileChange = (event: ChangeEvent) => { - const selectedFile = event.target.files?.[0] || null; - setFile(selectedFile); - }; - - // Handle file upload - const handleUpload = async () => { - if (!file) { - console.error("No file selected"); - return; - } - - const formData = new FormData(); - formData.append("file", file); - - try { - const response = await API.handleUpload(formData); - if (response) setImageURL(response.image_url); - } catch (error) { - console.error("Error uploading the file", error); - } - }; - - return ( - - - -

{message}

- - - {imageURL && ( -
-

Uploaded Image

- Uploaded file -

- Image URL:{" "} - - {imageURL} - -

-
- )} - -
-
- ); -}; - -export default Home; diff --git a/linguaphoto/api/image.py b/linguaphoto/api/image.py index d912370..e188bdb 100644 --- a/linguaphoto/api/image.py +++ b/linguaphoto/api/image.py @@ -3,18 +3,12 @@ from typing import Annotated, List from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile -from pydantic import BaseModel from linguaphoto.crud.collection import CollectionCrud from linguaphoto.crud.image import ImageCrud from linguaphoto.models import Image from linguaphoto.utils.auth import get_current_user_id, subscription_validate - -class TranslateFramgement(BaseModel): - images: List[str] - - router = APIRouter() @@ -64,11 +58,11 @@ async def delete_image( @router.post("/translate", response_model=List[Image]) async def translate( - data: TranslateFramgement, + data: List[str], user_id: str = Depends(get_current_user_id), image_crud: ImageCrud = Depends(), is_subscribed: bool = Depends(subscription_validate), ) -> List[Image]: async with image_crud: - images = await image_crud.translate(data.images, user_id=user_id) + images = await image_crud.translate(data, user_id=user_id) return images diff --git a/linguaphoto/api/subscription.py b/linguaphoto/api/subscription.py index 588a85e..e8dd6c8 100644 --- a/linguaphoto/api/subscription.py +++ b/linguaphoto/api/subscription.py @@ -1,7 +1,7 @@ """Collection API.""" import stripe -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from linguaphoto.crud.user import UserCrud from linguaphoto.settings import settings @@ -17,6 +17,7 @@ @router.post("/create_subscription") async def subscribe(data: dict, user_id: str = Depends(get_current_user_id), user_crud: UserCrud = Depends()) -> dict: + print(settings.openai_key) try: # Create a customer if it doesn't exist customer = stripe.Customer.create( @@ -43,4 +44,4 @@ async def subscribe(data: dict, user_id: str = Depends(get_current_user_id), use return {"success": True} except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + return {"success": False, "error": str(e)} diff --git a/linguaphoto/private_key.pem b/linguaphoto/private_key.pem new file mode 100644 index 0000000..094c641 --- /dev/null +++ b/linguaphoto/private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAxoAXnKN5nJlYe44iFBJJXgd+nH8WCJ/q+7yLE5iRAf5ylCHH +/Vai6ogLOPrQQzSyKLRAIBRxlgFw/U60H0wyiD8bS/iRy5Dlne4Vp2WHc/Lw6M+o +E0Fb/YiAM1dpPFFpHOKy/OL0CN5/eh0i7cvUht1Ssz+KI5Zgi4i7KuI67p2LdSZd ++rsqmBipyrTPTONPADgj1wuVq3J/6TX8ltMPiJgCMNblddDtn5OArWCZVsGydKmP +kHM051q4wJQn9Y2eI7ho6Qd3i4iuV7rK0T/gKu9kv9gdb6k1J/Le/jaEMcXuUSj8 +nu2RsfkMotxH/Rm6XH5LdjrcVWY4ulF7WgJk9QIDAQABAoIBAF6S2+0Y5BGs6//e +HbVzavo+VuAIGr7cNnBfCeI5v+jzyrJyD99PfkqAq9wnf79tZW7IRn8iTmXaZPOD +IoWA39iTPJWrJgeXjxb6Pt3lHS6ssoQxn9Igw0vd07ribDS9UvfcuMuM9BkfrRvI +swIaKbVh0Iuve8jt1izU5dwOMmbJ171MZHMiILsKH5Xf3UmkDTX1cLU3GoSBpBa+ +oq5ngo9MiWoBgBA7sexm6kQIcRJqJJGrRIL7sAYFJQd2C9kV4rnTcC8Vw/mD4G/U +oe5TuSQ98T+SAjgso+xyqA9uYgVRfE37h5ctYVf4t2zPD1LrkxV6gljk2VizSFg3 +4S/XUrcCgYEA/cs3L6lOcbxvFxHdrE6LL6c2hIiiJxUFGq/I4J/WYwMMDtMPgqNw +uwbhK+cQkRyO5YcinV7CRTON7LLFhNJZug1nAcVCVH7OOMUM6xef+6bIy8SVf/gT +0x69+x3oRMILHF5BLBpO+DsvTgsFSF2cDsN6S7LlJ+J2Xg2YlP7BRI8CgYEAyDnU +FAAD4elgOxDVTps+bt3fgZSmzSaAaVdL2hNgpA3SBvcGfq/tRntKCt0oFUn9z//y +Y2+7rTN5A6cJNdPQGA2HE1YtECIO97ir3gDypGxZatCGywR90KTOBnQVWndDqddT +lOQxIFRfokEPmNtQP+D4fVKZ51mifKTXa5RK6DsCgYEA5SkhY3/UvQ4QoRwYtQUW +2Kh7qaBWCkQIn9gp4elxg2W09Y3Oa394wuerWiEB7IWE4evrbX2qnSG3/QpPH2dw +bXa8k/Sxt+nn+4qx53Ull+05UgTnmO0/uVoA4UZX+/3aWnshDdmThMCsLiP1WSpt +R0dqnf+iuyjZCIPuSlrd2DECgYAfggIel5YE0d3DzbVTZlifx6hpUsQg2mMwsH0O +NyhpCIe5ctwByZt0EOio5v5swzT+q08wWJ/W9JehfIKVhtxjPJW59ECpHkLuto/N +IqcMOsSja2cawX0u/RAysce+cbAjJPBRKMuWQ9C8zrIuoqxxMOzJg9sWLePE64e+ +tRpIiwKBgQCctJAzR66e4RsBIY5AaxSLbP17qPbQKeuwbFrG6xbQbQQ8hqPuxPW9 +dDTflbkoSK+paqHsd1yiLX+IwylTZWPdTBUdpO9in0f/WYm1DGZEXyR0MtFv7NFG +bETRchnR3ss+6ZYIbn/AtZWEcGW3YwX4Ys3+5RGfRM6TeKT+cGj08A== +-----END RSA PRIVATE KEY----- diff --git a/linguaphoto/utils/cloudfront_url_signer.py b/linguaphoto/utils/cloudfront_url_signer.py index 3697dcc..a7e6356 100644 --- a/linguaphoto/utils/cloudfront_url_signer.py +++ b/linguaphoto/utils/cloudfront_url_signer.py @@ -24,7 +24,7 @@ def __init__(self, key_id: str, private_key_path: str) -> None: self.private_key_path = private_key_path self.cf_signer = CloudFrontSigner(key_id, self._rsa_signer) - def _rsa_signer(self, message: str) -> bytes: + def _rsa_signer(self, message: bytes) -> bytes: """RSA signer function that signs a message using the private key. :param message: The message to be signed. @@ -33,7 +33,7 @@ def _rsa_signer(self, message: str) -> bytes: with open(self.private_key_path, "r") as key_file: private_key = key_file.read() return rsa.sign( - message.encode("utf8"), # Ensure message is in bytes + message, # Ensure message is in bytes rsa.PrivateKey.load_pkcs1(private_key.encode("utf8")), "SHA-1", # CloudFront requires SHA-1 hash ) From dc14c38cd50cc31bb6a65858f6849a7e9a6372e6 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 2 Oct 2024 20:04:04 +0300 Subject: [PATCH 06/13] chore_module_insert --- frontend/package-lock.json | 14 ++++++++++++++ frontend/package.json | 1 + 2 files changed, 15 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b45773..da2f963 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "clsx": "^2.1.1", "holderjs": "^2.9.9", "nth-check": "^2.1.1", + "openapi-fetch": "^0.12.2", "react": "^18.3.1", "react-beautiful-dnd-grid": "^0.1.3-alpha", "react-bootstrap-icons": "^1.11.4", @@ -14332,6 +14333,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-fetch": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.12.2.tgz", + "integrity": "sha512-ctMQ4LkkSWfIDUMuf1SYuPMsQ7ePcWAkYaMPW1lCDdk4WlV3Vulq1zoyGrwnFVvrBs5t7OOqNF+EKa8SAaovEA==", + "dependencies": { + "openapi-typescript-helpers": "^0.0.13" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.13.tgz", + "integrity": "sha512-z44WK2e7ygW3aUtAtiurfEACohf/Qt9g6BsejmIYgEoY4REHeRzgFJmO3ium0libsuzPc145I+8lE9aiiZrQvQ==" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 54d356d..1b0c51a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "clsx": "^2.1.1", "holderjs": "^2.9.9", "nth-check": "^2.1.1", + "openapi-fetch": "^0.12.2", "react": "^18.3.1", "react-beautiful-dnd-grid": "^0.1.3-alpha", "react-bootstrap-icons": "^1.11.4", From fe2964e8da03a497360b10fee91a5addf00972b2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 2 Oct 2024 20:34:46 +0300 Subject: [PATCH 07/13] fix_edit_collection_fix --- frontend/src/gen/api.ts | 6 +++++- frontend/src/pages/Collection.tsx | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index d865f9f..4d505c4 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -1596,7 +1596,11 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: Collection; + requestBody: { + content: { + "application/json": Collection; + }; + }; responses: { /** @description Successful Response */ 200: { diff --git a/frontend/src/pages/Collection.tsx b/frontend/src/pages/Collection.tsx index b77d9d7..a7ca922 100644 --- a/frontend/src/pages/Collection.tsx +++ b/frontend/src/pages/Collection.tsx @@ -187,7 +187,9 @@ const CollectionPage: React.FC = () => { const asyncfunction = async () => { startLoading(); collection.images = reorderImageIds; - const { error } = await client.POST("/edit_collection", { collection }); + const { error } = await client.POST("/edit_collection", { + body: collection, + }); if (error) addAlert(error.detail?.toString(), "error"); else { setCollection({ ...collection }); From c960c2bd00e9bf525a04e51f7f3e214bbd050f6f Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 14 Oct 2024 17:09:35 +0300 Subject: [PATCH 08/13] fix_frontend_critical_update --- frontend/package-lock.json | 217 ----- frontend/package.json | 2 - frontend/src/App.css | 90 ++- frontend/src/App.tsx | 12 +- frontend/src/ProtectedRoute.tsx | 19 +- frontend/src/components/Audio.tsx | 52 +- frontend/src/components/Book.tsx | 91 +++ frontend/src/components/BookSkeleton.tsx | 40 + frontend/src/components/HOC/Container.tsx | 10 + frontend/src/components/HOC/Content.tsx | 15 + .../src/components/HOC/DraggableWrapper.tsx | 78 ++ frontend/src/components/Logo.tsx | 14 + frontend/src/components/UploadContent.tsx | 6 +- .../components/auth/GoogleAuthComponent.tsx | 89 --- .../components/auth/RequireAuthentication.tsx | 22 - frontend/src/components/card.tsx | 2 +- frontend/src/components/collection/Edit.tsx | 350 ++++++++ frontend/src/components/collection/New.tsx | 62 ++ frontend/src/components/collection/View.tsx | 170 ++++ frontend/src/components/image.tsx | 6 - frontend/src/components/modal.tsx | 6 +- frontend/src/components/nav/Navbar.tsx | 107 +++ frontend/src/components/nav/TopNavbar.tsx | 123 --- frontend/src/components/new_card.tsx | 19 +- frontend/src/contexts/AuthContext.tsx | 7 +- frontend/src/contexts/api.tsx | 18 - frontend/src/gen/api.ts | 750 +++--------------- frontend/src/hooks/alerts.tsx | 81 +- frontend/src/hooks/auth.tsx | 144 ---- frontend/src/hooks/theme.tsx | 2 +- frontend/src/images/KScaleASCII.png | Bin 0 -> 58574 bytes frontend/src/images/KScaleASCIIMobile.png | Bin 0 -> 42819 bytes frontend/src/images/bookplaceholder.png | Bin 0 -> 298586 bytes frontend/src/images/small-logo.png | Bin 0 -> 25094 bytes frontend/src/pages/Collection.tsx | 541 ++----------- frontend/src/pages/Collections.tsx | 56 +- frontend/src/pages/Home.tsx | 134 +++- frontend/src/pages/Login.tsx | 26 +- frontend/src/pages/NotFound.tsx | 10 +- frontend/src/pages/Subscription.tsx | 23 +- frontend/src/types/model.ts | 3 + frontend/tailwind.config.js | 50 +- linguaphoto/api/collection.py | 28 +- linguaphoto/crud/base.py | 2 +- linguaphoto/crud/collection.py | 9 + linguaphoto/models.py | 2 + linguaphoto/schemas/collection.py | 6 + 47 files changed, 1557 insertions(+), 1937 deletions(-) create mode 100644 frontend/src/components/Book.tsx create mode 100644 frontend/src/components/BookSkeleton.tsx create mode 100644 frontend/src/components/HOC/Container.tsx create mode 100644 frontend/src/components/HOC/Content.tsx create mode 100644 frontend/src/components/HOC/DraggableWrapper.tsx create mode 100644 frontend/src/components/Logo.tsx delete mode 100644 frontend/src/components/auth/GoogleAuthComponent.tsx delete mode 100644 frontend/src/components/auth/RequireAuthentication.tsx create mode 100644 frontend/src/components/collection/Edit.tsx create mode 100644 frontend/src/components/collection/New.tsx create mode 100644 frontend/src/components/collection/View.tsx create mode 100644 frontend/src/components/nav/Navbar.tsx delete mode 100644 frontend/src/components/nav/TopNavbar.tsx delete mode 100644 frontend/src/hooks/auth.tsx create mode 100644 frontend/src/images/KScaleASCII.png create mode 100644 frontend/src/images/KScaleASCIIMobile.png create mode 100644 frontend/src/images/bookplaceholder.png create mode 100644 frontend/src/images/small-logo.png diff --git a/frontend/package-lock.json b/frontend/package-lock.json index da2f963..79ac845 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -44,7 +44,6 @@ "@types/jest": "^29.5.12", "axios": "^1.7.2", "babel-eslint": "*", - "bootstrap": "^5.3.3", "eslint": "^8.0.0", "eslint-config-prettier": "*", "eslint-import-resolver-typescript": "*", @@ -56,7 +55,6 @@ "nodemon": "^3.1.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", - "react-bootstrap": "^2.10.2", "react-router-dom": "^6.23.1", "tailwindcss": "^3.4.10", "ts-jest": "^29.2.5", @@ -3602,31 +3600,6 @@ "node": ">= 8" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz", - "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==", - "dev": true, - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@react-oauth/google": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", @@ -3646,48 +3619,6 @@ "node": ">=14.0.0" } }, - "node_modules/@restart/hooks": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", - "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@restart/ui": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.8.0.tgz", - "integrity": "sha512-xJEOXUOTmT4FngTmhdjKFRrVVF0hwCLNPdatLCHkyS4dkiSK12cEu1Y0fjxktjJrdst9jJIc5J6ihMJCoWEN/g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.21.0", - "@popperjs/core": "^2.11.6", - "@react-aria/ssr": "^3.5.0", - "@restart/hooks": "^0.4.9", - "@types/warning": "^3.0.0", - "dequal": "^2.0.3", - "dom-helpers": "^5.2.0", - "uncontrollable": "^8.0.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - } - }, - "node_modules/@restart/ui/node_modules/uncontrollable": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", - "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", - "dev": true, - "peerDependencies": { - "react": ">=16.14.0" - } - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4055,15 +3986,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@swc/helpers": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", - "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -4645,15 +4567,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -4725,12 +4638,6 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, - "node_modules/@types/warning": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", - "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", - "dev": true - }, "node_modules/@types/ws": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -6142,25 +6049,6 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, - "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6437,12 +6325,6 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==" }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "dev": true - }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -7542,16 +7424,6 @@ "utila": "~0.4" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -16049,25 +15921,6 @@ "react-is": "^16.13.1" } }, - "node_modules/prop-types-extra": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", - "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", - "dev": true, - "dependencies": { - "react-is": "^16.3.2", - "warning": "^4.0.0" - }, - "peerDependencies": { - "react": ">=0.14.0" - } - }, - "node_modules/prop-types-extra/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -16359,36 +16212,6 @@ "uuid": "bin/uuid" } }, - "node_modules/react-bootstrap": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.4.tgz", - "integrity": "sha512-W3398nBM2CBfmGP2evneEO3ZZwEMPtHs72q++eNw60uDGDAdiGn0f9yNys91eo7/y8CTF5Ke1C0QO8JFVPU40Q==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.24.7", - "@restart/hooks": "^0.4.9", - "@restart/ui": "^1.6.9", - "@types/react-transition-group": "^4.4.6", - "classnames": "^2.3.2", - "dom-helpers": "^5.2.1", - "invariant": "^2.2.4", - "prop-types": "^15.8.1", - "prop-types-extra": "^1.1.0", - "react-transition-group": "^4.4.5", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "@types/react": ">=16.14.8", - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-bootstrap-icons": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.4.tgz", @@ -17988,22 +17811,6 @@ "node": ">=10" } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -20494,21 +20301,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -20734,15 +20526,6 @@ "makeerror": "1.0.12" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1b0c51a..5cb1a8d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -61,7 +61,6 @@ "@types/jest": "^29.5.12", "axios": "^1.7.2", "babel-eslint": "*", - "bootstrap": "^5.3.3", "eslint": "^8.0.0", "eslint-config-prettier": "*", "eslint-import-resolver-typescript": "*", @@ -73,7 +72,6 @@ "nodemon": "^3.1.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", - "react-bootstrap": "^2.10.2", "react-router-dom": "^6.23.1", "tailwindcss": "^3.4.10", "ts-jest": "^29.2.5", diff --git a/frontend/src/App.css b/frontend/src/App.css index 56f0752..43ca4da 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -4,5 +4,93 @@ @tailwind utilities; body { - font-family: "Noto Serif SC", serif; /* Use Noto Serif Simplified Chinese font */ + font-family: "Noto Serif SC", serif; + /* Use Noto Serif Simplified Chinese font */ +} + +.perspective { + perspective: 1000px; +} + +.transform-style-preserve-3d { + transform-style: preserve-3d; +} + +.backface-hidden { + backface-visibility: hidden; +} + +.group-hover\:rotate-y-30:hover, +.rotate-y-30 { + transform: rotateY(-30deg); + /* Rotate the element on hover */ +} + +.rotate-y-90 { + transform: rotateY(-90deg) !important; +} + +.rotate-y-180 { + transform: rotateY(180deg); +} + +.rotate-left { + transform-origin: left; + /* Set transform origin to the left edge */ +} + +@layer base { + @layer base { + /* Global input customization */ + input, + textarea, + select { + @apply border border-gray-300 rounded-lg px-4 py-2 bg-white text-black placeholder-gray-400 transition duration-200 ease-in-out; + } + + /* Hover and focus states for inputs */ + input:hover, + textarea:hover, + select:hover { + @apply border-gray-400; + } + + input:focus, + textarea:focus, + select:focus { + @apply border-blue-500 ring ring-blue-200 outline-none; + } + + /* Disabled input */ + input:disabled, + textarea:disabled, + select:disabled { + @apply bg-gray-200 cursor-not-allowed opacity-50; + } + + /* Additional customization for textareas */ + textarea { + @apply resize-none; + /* Prevents resizing */ + } + + /* Button styling */ + button { + @apply bg-blue-500 text-white px-6 py-2 rounded-lg transition duration-200 ease-in-out; + } + + /* Hover and focus states for buttons */ + button:hover { + @apply bg-blue-600; + } + + button:focus { + @apply ring ring-blue-300 outline-none; + } + + /* Disabled button */ + button:disabled { + @apply bg-gray-500 cursor-not-allowed opacity-50; + } + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d5175f8..b9cbbfe 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ -import "bootstrap/dist/css/bootstrap.min.css"; +import Content from "components/HOC/Content"; import LoadingMask from "components/LoadingMask"; -import TopNavbar from "components/nav/TopNavbar"; +import Navbar from "components/nav/Navbar"; import NotFoundRedirect from "components/NotFoundRedirect"; import { AuthProvider } from "contexts/AuthContext"; import { LoadingProvider } from "contexts/LoadingContext"; @@ -14,7 +14,6 @@ import NotFound from "pages/NotFound"; import SubscriptionTypePage from "pages/SubscriptioinType"; import SubscriptionCancelPage from "pages/Subscription"; import PrivateRoute from "ProtectedRoute"; -import { Container } from "react-bootstrap"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import "./App.css"; @@ -26,7 +25,7 @@ const App = () => { - + } /> } /> @@ -82,8 +81,9 @@ const App = () => { /> } /> - - + + + {/* */} diff --git a/frontend/src/ProtectedRoute.tsx b/frontend/src/ProtectedRoute.tsx index 438242c..42bfee8 100644 --- a/frontend/src/ProtectedRoute.tsx +++ b/frontend/src/ProtectedRoute.tsx @@ -11,20 +11,19 @@ const PrivateRoute: React.FC = ({ element, requiredSubscription = false, }) => { - const { auth } = useAuth(); + const { auth, is_auth } = useAuth(); const location = useLocation(); const navigate = useNavigate(); useEffect(() => { - if (auth) - if (!auth?.is_auth) { - // Redirect to login if not authenticated - navigate("/login", { replace: true, state: { from: location } }); - } else if (requiredSubscription && !auth?.is_subscription) { - // Redirect to subscription page if subscription is required and not active - navigate("/subscription_type", { replace: true }); - } - }, [auth, requiredSubscription, navigate, location]); + if (!is_auth) { + // Redirect to login if not authenticated + navigate("/login", { replace: true, state: { from: location } }); + } else if (auth && requiredSubscription && !auth?.is_subscription) { + // Redirect to subscription page if subscription is required and not active + navigate("/subscription_type", { replace: true }); + } + }, [auth, is_auth, requiredSubscription, navigate, location]); if (!auth?.is_auth || (requiredSubscription && !auth?.is_subscription)) { // Render nothing while redirecting diff --git a/frontend/src/components/Audio.tsx b/frontend/src/components/Audio.tsx index 5dea933..9d92b1f 100644 --- a/frontend/src/components/Audio.tsx +++ b/frontend/src/components/Audio.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { CaretLeft, CaretRight } from "react-bootstrap-icons"; import { FaPause, FaPlay, @@ -9,15 +10,22 @@ import { import { Image } from "types/model"; interface AudioPlayerProps { - currentImage: Image; - index: number; + currentImage: Image; // current image + index: number; // current transcription index + handleTranscriptionNext: () => void; //next transcript + handleTranscriptionPrev: () => void; //prev transcript } -const AudioPlayer: React.FC = ({ currentImage, index }) => { - const audioRef = useRef(null); +const AudioPlayer: React.FC = ({ + currentImage, + index, + handleTranscriptionNext, + handleTranscriptionPrev, +}) => { + const audioRef = useRef(null); //audio const [isPlaying, setIsPlaying] = useState(false); - const [playbackRate, setPlaybackRate] = useState(1); - const [volume, setVolume] = useState(1); + const [playbackRate, setPlaybackRate] = useState(1); //speed + const [volume, setVolume] = useState(1); // volumn size const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); @@ -82,13 +90,15 @@ const AudioPlayer: React.FC = ({ currentImage, index }) => { setCurrentTime(0); // Reset current time to 0 }); + audio.addEventListener("ended", handleTranscriptionNext); + // Clean up the event listeners on component unmount return () => { audio.removeEventListener("timeupdate", () => {}); audio.removeEventListener("ended", () => {}); }; } - }, []); + }, [handleTranscriptionNext]); useEffect(() => { if (audioRef.current) { @@ -98,7 +108,7 @@ const AudioPlayer: React.FC = ({ currentImage, index }) => { }, [currentImage, index]); return ( -
+