├── LICENSE ├── Procfile ├── README.md ├── app ├── admin │ ├── models.py │ ├── routes.py │ └── utils.py ├── auth │ ├── jwt_handler.py │ ├── models.py │ ├── password_handler.py │ └── routes.py ├── database.py ├── nft │ ├── models.py │ ├── routes.py │ └── utils.py └── user │ ├── models.py │ ├── routes.py │ └── utils.py ├── main.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Chalana Devinda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn main:app --host 0.0.0.0 --port ${PORT:-5000} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pixora API 2 | 3 | A FastAPI application that provides authentication and user management functionality with MongoDB integration. 4 | 5 | ## Features 6 | 7 | - User Authentication 8 | - Sign up with email and password 9 | - Login with JWT authentication 10 | - Password hashing for security 11 | - User Management 12 | - User profile retrieval 13 | - Protected endpoints using JWT 14 | - MongoDB Integration 15 | - Connects to MongoDB Atlas cluster 16 | - Stores user data securely 17 | 18 | ## Technologies Used 19 | 20 | - FastAPI: Modern, fast web framework for building APIs 21 | - MongoDB: NoSQL database for storing user data 22 | - Motor: Asynchronous MongoDB driver for Python 23 | - PyJWT: JSON Web Token implementation for Python 24 | - Pydantic v2: Data validation and settings management 25 | - Passlib & Bcrypt: Password hashing 26 | 27 | ## Project Structure 28 | 29 | project/ │ ├── .env ├── main.py ├── requirements.txt │ ├── app/ │ ├── init.py │ ├── database.py │ │ │ ├── models/ │ │ ├── init.py │ │ └── user_model.py │ │ │ ├── auth/ │ │ ├── init.py │ │ ├── routes.py │ │ ├── jwt_handler.py │ │ └── password_handler.py │ │ │ └── user/ │ ├── init.py │ ├── routes.py │ └── utils.py 30 | 31 | 32 | ## API Endpoints 33 | 34 | ### Authentication 35 | 36 | - **POST** `/api/auth/signup` - Register a new user 37 | - Body Parameters: 38 | - `first_name` (string): User's first name 39 | - `last_name` (string): User's last name 40 | - `email` (string): User's email 41 | - `password` (string): User's password (with validation rules) 42 | - `contact` (string): User's phone number 43 | - `birthday` (string): User's birthday (YYYY-MM-DD) 44 | - Returns: Success message with user ID 45 | 46 | - **POST** `/api/auth/login` - Authenticate a user 47 | - Form Parameters: 48 | - `username` (string): User's email 49 | - `password` (string): User's password 50 | - Returns: JWT access token 51 | 52 | ### User Management 53 | 54 | - **GET** `/api/users/me` - Get current user details 55 | - Headers: Bearer token 56 | - Returns: User profile data 57 | 58 | - **GET** `/api/users/{user_id}` - Get user by ID 59 | - Headers: Bearer token 60 | - Returns: User profile data (only if requesting own profile) 61 | 62 | ## Setup and Installation 63 | 64 | 1. Clone the repository 65 | 2. Create a virtual environment: 66 | 3. 67 | -------------------------------------------------------------------------------- /app/admin/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, EmailStr 2 | from datetime import datetime 3 | 4 | 5 | class VerificationRequestModel(BaseModel): 6 | id: str 7 | user_id: str 8 | user_email: EmailStr 9 | user_name: str 10 | address: str 11 | id_front_image: str 12 | id_back_image: str 13 | about_user_article_link: str 14 | status: str 15 | request_date: datetime 16 | profile_image: str 17 | 18 | class Config: 19 | schema_extra = { 20 | "example": { 21 | "id": "6829f883bb7e3d091bb3abcb", 22 | "user_id": "680694d44f57793badb91516", 23 | "user_email": "john.doe@example.com", 24 | "user_name": "John Doe", 25 | "address": "123 Main St, City, Country", 26 | "id_front_image": "base64_encoded_string...", 27 | "id_back_image": "base64_encoded_string...", 28 | "about_user_article_link": "https://example.com/about-user", 29 | "status": "pending", 30 | "request_date": "2025-05-18T07:46:02.978+00:00", 31 | "profile_image": "base64_encoded_profile_image..." 32 | } 33 | } -------------------------------------------------------------------------------- /app/admin/routes.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | from fastapi import APIRouter, HTTPException 3 | from app.database import get_db 4 | from app.admin.utils import format_verification_request 5 | 6 | admin_router = APIRouter() 7 | 8 | 9 | @admin_router.get("/verification-requests", response_model=list) 10 | async def get_all_verification_requests(): 11 | """ 12 | Fetch all verification requests without authorization 13 | """ 14 | db = await get_db() 15 | 16 | cursor = db["VerificationRequests"].find().sort("request_date", -1) # Sort by request_date descending 17 | verification_requests = [] 18 | 19 | async for request in cursor: 20 | formatted_request = format_verification_request(request) 21 | verification_requests.append(formatted_request) 22 | 23 | if not verification_requests: 24 | raise HTTPException(status_code=404, detail="No verification requests found") 25 | 26 | return verification_requests 27 | 28 | 29 | @admin_router.get("/pending-verification-requests", response_model=list) 30 | async def get_pending_verification_requests(): 31 | """ 32 | Fetch all pending verification requests 33 | """ 34 | db = await get_db() 35 | 36 | cursor = db["VerificationRequests"].find({"status": "pending"}).sort("request_date", -1) # Sort by request_date descending 37 | verification_requests = [] 38 | 39 | async for request in cursor: 40 | formatted_request = format_verification_request(request) 41 | verification_requests.append(formatted_request) 42 | 43 | if not verification_requests: 44 | raise HTTPException(status_code=404, detail="No pending verification requests found") 45 | 46 | return verification_requests 47 | 48 | 49 | @admin_router.get("/approved-verification-requests", response_model=list) 50 | async def get_approved_verification_requests(): 51 | """ 52 | Fetch all approved verification requests 53 | """ 54 | db = await get_db() 55 | 56 | cursor = db["VerificationRequests"].find({"status": "approved"}).sort("request_date", -1) # Sort by request_date descending 57 | verification_requests = [] 58 | 59 | async for request in cursor: 60 | formatted_request = format_verification_request(request) 61 | verification_requests.append(formatted_request) 62 | 63 | if not verification_requests: 64 | raise HTTPException(status_code=404, detail="No approved verification requests found") 65 | 66 | return verification_requests 67 | 68 | 69 | @admin_router.get("/rejected-verification-requests", response_model=list) 70 | async def get_rejected_verification_requests(): 71 | """ 72 | Fetch all rejected verification requests 73 | """ 74 | db = await get_db() 75 | 76 | cursor = db["VerificationRequests"].find({"status": "rejected"}).sort("request_date", -1) # Sort by request_date descending 77 | verification_requests = [] 78 | 79 | async for request in cursor: 80 | formatted_request = format_verification_request(request) 81 | verification_requests.append(formatted_request) 82 | 83 | if not verification_requests: 84 | raise HTTPException(status_code=404, detail="No rejected verification requests found") 85 | 86 | return verification_requests 87 | 88 | 89 | @admin_router.put("/verification-requests/{request_id}/status") 90 | async def update_verification_request_status(request_id: str, status: str): 91 | """ 92 | Update the status of a verification request to 'approved' or 'rejected' 93 | """ 94 | if status not in ["approved", "rejected"]: 95 | raise HTTPException(status_code=400, detail="Invalid status. Allowed values are 'approved' or 'rejected'.") 96 | 97 | db = await get_db() 98 | 99 | # Find the verification request by ID 100 | request = await db["VerificationRequests"].find_one({"_id": ObjectId(request_id)}) 101 | 102 | if not request: 103 | raise HTTPException(status_code=404, detail="Verification request not found") 104 | 105 | if request["status"] != "pending": 106 | raise HTTPException(status_code=400, detail="Only pending requests can be updated") 107 | 108 | # Update the status 109 | result = await db["VerificationRequests"].update_one( 110 | {"_id": ObjectId(request_id)}, 111 | {"$set": {"status": status}} 112 | ) 113 | 114 | if result.modified_count == 0: 115 | raise HTTPException(status_code=500, detail="Failed to update status") 116 | 117 | return {"message": f"Verification request {request_id} status updated to {status}"} -------------------------------------------------------------------------------- /app/admin/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from bson import ObjectId 3 | 4 | 5 | def format_verification_request(request): 6 | """ 7 | Format verification request data for response 8 | """ 9 | return { 10 | "id": str(request["_id"]), 11 | "user_id": request["user_id"], 12 | "user_email": request["user_email"], 13 | "user_name": request["user_name"], 14 | "address": request["address"], 15 | "id_front_image": request["id_front_image"], 16 | "id_back_image": request["id_back_image"], 17 | "about_user_article_link": request["about_user_article_link"], 18 | "status": request["status"], 19 | "request_date": request["request_date"].strftime("%Y-%m-%d %H:%M:%S") 20 | if isinstance(request["request_date"], datetime) 21 | else request["request_date"], 22 | "profile_image": request["profile_image"], 23 | } -------------------------------------------------------------------------------- /app/auth/jwt_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime, timedelta 3 | from typing import Dict, Optional 4 | 5 | from jose import jwt, JWTError 6 | from fastapi import Depends, HTTPException, status 7 | from fastapi.security import OAuth2PasswordBearer 8 | from dotenv import load_dotenv 9 | from app.database import get_db 10 | 11 | load_dotenv() 12 | 13 | # Configuration 14 | JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") 15 | JWT_ALGORITHM = os.getenv("JWT_ALGORITHM") 16 | ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30)) 17 | 18 | # OAuth2 scheme for token authentication 19 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") 20 | 21 | 22 | def create_access_token(data: Dict, expires_delta: Optional[timedelta] = None) -> str: 23 | """ 24 | Create a JWT access token 25 | """ 26 | to_encode = data.copy() 27 | 28 | # Set expiration time 29 | if expires_delta: 30 | expire = datetime.utcnow() + expires_delta 31 | else: 32 | expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 33 | 34 | to_encode.update({"exp": expire}) 35 | 36 | # Create JWT token 37 | encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) 38 | return encoded_jwt 39 | 40 | 41 | async def get_current_user(token: str = Depends(oauth2_scheme)): 42 | """ 43 | Validate the token and return the current user 44 | """ 45 | credentials_exception = HTTPException( 46 | status_code=status.HTTP_401_UNAUTHORIZED, 47 | detail="Could not validate credentials", 48 | headers={"WWW-Authenticate": "Bearer"}, 49 | ) 50 | 51 | try: 52 | # Decode the token 53 | payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) 54 | user_id: str = payload.get("sub") 55 | 56 | if user_id is None: 57 | raise credentials_exception 58 | 59 | # Get user from database 60 | db = await get_db() 61 | user = await db["users"].find_one({"_id": user_id}) 62 | 63 | if user is None: 64 | raise credentials_exception 65 | 66 | # Convert MongoDB ObjectId to string for response 67 | user["id"] = str(user["_id"]) 68 | del user["_id"] 69 | del user["password"] 70 | 71 | return user 72 | 73 | except JWTError: 74 | raise credentials_exception -------------------------------------------------------------------------------- /app/auth/models.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime, timezone 2 | from pydantic import BaseModel, EmailStr, Field 3 | import re 4 | 5 | 6 | # User model for database 7 | class UserModel(BaseModel): 8 | first_name: str 9 | last_name: str 10 | email: EmailStr 11 | password: str 12 | contact: str 13 | birthday: date 14 | created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 15 | updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) 16 | 17 | class Config: 18 | json_schema_extra = { 19 | "example": { 20 | "first_name": "John", 21 | "last_name": "Doe", 22 | "email": "john.doe@example.com", 23 | "password": "StrongPassword123!", 24 | "contact": "+1234567890", 25 | "birthday": "1990-01-01" 26 | } 27 | } 28 | 29 | 30 | # Schema for user sign-up 31 | class UserSignUp(BaseModel): 32 | first_name: str = Field(..., min_length=1, max_length=50) 33 | last_name: str = Field(..., min_length=1, max_length=50) 34 | email: EmailStr 35 | password: str = Field(..., min_length=8) 36 | contact: str 37 | birthday: date 38 | 39 | @classmethod 40 | def strong_password(cls, v): 41 | if not re.search(r'[A-Z]', v): 42 | raise ValueError('Password must contain at least one uppercase letter') 43 | if not re.search(r'[a-z]', v): 44 | raise ValueError('Password must contain at least one lowercase letter') 45 | if not re.search(r'[0-9]', v): 46 | raise ValueError('Password must contain at least one digit') 47 | if not re.search(r'[!@#$%^&*(),.?":{}|<>]', v): 48 | raise ValueError('Password must contain at least one special character') 49 | return v 50 | 51 | @classmethod 52 | def validate_contact(cls, v): 53 | if not re.match(r'^\+?[0-9]{10,15}$', v): 54 | raise ValueError('Contact must be a valid phone number (10-15 digits with optional + prefix)') 55 | return v 56 | 57 | class Config: 58 | json_schema_extra = { 59 | "example": { 60 | "first_name": "John", 61 | "last_name": "Doe", 62 | "email": "john.doe@example.com", 63 | "password": "StrongPassword123!", 64 | "contact": "+1234567890", 65 | "birthday": "1990-01-01" 66 | } 67 | } 68 | 69 | 70 | # Schema for user login 71 | class UserLogin(BaseModel): 72 | email: EmailStr 73 | password: str 74 | 75 | class Config: 76 | json_schema_extra = { 77 | "example": { 78 | "email": "john.doe@example.com", 79 | "password": "StrongPassword123!" 80 | } 81 | } 82 | 83 | 84 | # Schema for token response 85 | class TokenResponse(BaseModel): 86 | access_token: str 87 | token_type: str = "bearer" 88 | 89 | 90 | # Schema for user response (excluding sensitive information) 91 | class UserResponse(BaseModel): 92 | id: str 93 | first_name: str 94 | last_name: str 95 | email: EmailStr 96 | contact: str 97 | birthday: date 98 | created_at: datetime 99 | updated_at: datetime -------------------------------------------------------------------------------- /app/auth/password_handler.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 4 | 5 | def hash_password(password: str) -> str: 6 | """ 7 | Hash the password using bcrypt 8 | """ 9 | return pwd_context.hash(password) 10 | 11 | def verify_password(plain_password: str, hashed_password: str) -> bool: 12 | """ 13 | Verify the password against its hashed version 14 | """ 15 | return pwd_context.verify(plain_password, hashed_password) -------------------------------------------------------------------------------- /app/auth/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends, HTTPException, status 2 | from fastapi.security import OAuth2PasswordRequestForm 3 | from datetime import datetime, timedelta 4 | import os 5 | from bson import ObjectId 6 | 7 | from app.auth.models import UserSignUp, UserLogin, TokenResponse 8 | from app.auth.password_handler import hash_password, verify_password 9 | from app.auth.jwt_handler import create_access_token 10 | from app.database import get_db 11 | 12 | auth_router = APIRouter() 13 | 14 | 15 | @auth_router.post("/signup", response_model=dict, status_code=status.HTTP_201_CREATED) 16 | async def create_user(user_data: UserSignUp = Body(...)): 17 | db = await get_db() 18 | 19 | # Check if user with email already exists 20 | user_exists = await db["users"].find_one({"email": user_data.email}) 21 | if user_exists: 22 | raise HTTPException( 23 | status_code=status.HTTP_409_CONFLICT, 24 | detail="User with this email already exists" 25 | ) 26 | 27 | # Hash the password 28 | hashed_password = hash_password(user_data.password) 29 | 30 | # Create user dict with hashed password 31 | new_user = { 32 | "_id": str(ObjectId()), 33 | "first_name": user_data.first_name, 34 | "last_name": user_data.last_name, 35 | "email": user_data.email, 36 | "password": hashed_password, 37 | "contact": user_data.contact, 38 | "birthday": user_data.birthday.strftime("%Y-%m-%d"), 39 | "created_at": datetime.utcnow(), 40 | "updated_at": datetime.utcnow() 41 | } 42 | 43 | # Insert user into database 44 | await db["users"].insert_one(new_user) 45 | 46 | return { 47 | "message": "User created successfully", 48 | "id": new_user["_id"] 49 | } 50 | 51 | 52 | @auth_router.post("/login", response_model=TokenResponse) 53 | async def login(form_data: OAuth2PasswordRequestForm = Depends()): 54 | db = await get_db() 55 | 56 | # Find user by email 57 | user = await db["users"].find_one({"email": form_data.username}) 58 | if not user: 59 | raise HTTPException( 60 | status_code=status.HTTP_401_UNAUTHORIZED, 61 | detail="Incorrect email or password", 62 | headers={"WWW-Authenticate": "Bearer"}, 63 | ) 64 | 65 | # Verify password 66 | if not verify_password(form_data.password, user["password"]): 67 | raise HTTPException( 68 | status_code=status.HTTP_401_UNAUTHORIZED, 69 | detail="Incorrect email or password", 70 | headers={"WWW-Authenticate": "Bearer"}, 71 | ) 72 | 73 | # Create access token 74 | access_token_expires = timedelta( 75 | minutes=int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES")) 76 | ) 77 | access_token = create_access_token( 78 | data={"sub": user["_id"]}, 79 | expires_delta=access_token_expires 80 | ) 81 | 82 | return {"access_token": access_token, "token_type": "bearer"} -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | from motor.motor_asyncio import AsyncIOMotorClient 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | class Database: 8 | client: AsyncIOMotorClient = None 9 | db_name: str = None 10 | 11 | db = Database() 12 | 13 | # Initialize database connection right away, not just during startup event 14 | mongodb_uri = os.getenv("MONGODB_URI") 15 | db.db_name = os.getenv("DB_NAME", "pixora_db") 16 | db.client = AsyncIOMotorClient(mongodb_uri) 17 | 18 | async def init_db(): 19 | # Ensure indexes for better query performance and unique constraints 20 | await db.client[db.db_name]["users"].create_index("email", unique=True) 21 | 22 | # index for VerificationRequests collection 23 | # Compound index for user_id and request_date to optimize the query 24 | await db.client[db.db_name]["VerificationRequests"].create_index([ 25 | ("user_id", 1), # 1 for ascending order 26 | ("request_date", -1) # -1 for descending order 27 | ]) 28 | 29 | # Add index for status field which will be used in queries 30 | await db.client[db.db_name]["VerificationRequests"].create_index("status") 31 | 32 | # Ensure indexes for better query performance and unique constraints 33 | await db.client[db.db_name]["users"].create_index("email", unique=True) 34 | print("Connected to MongoDB!") 35 | 36 | async def get_db(): 37 | return db.client[db.db_name] -------------------------------------------------------------------------------- /app/nft/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from motor.motor_asyncio import AsyncIOMotorClient 4 | 5 | load_dotenv() 6 | MONGODB_URI = os.getenv("MONGODB_URI") 7 | DB_NAME = os.getenv("DB_NAME") 8 | if not MONGODB_URI or not DB_NAME: 9 | raise Exception("MONGODB_URI and DB_NAME must be set in the .env file.") 10 | 11 | mongo_client = AsyncIOMotorClient(MONGODB_URI) 12 | db = mongo_client[DB_NAME] 13 | nft_collection = db["NFT"] -------------------------------------------------------------------------------- /app/nft/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Form, Query 2 | from fastapi.responses import JSONResponse 3 | 4 | from .models import nft_collection 5 | from .utils import upload_image_to_api, get_user_info_from_api 6 | 7 | from typing import Optional, List 8 | 9 | nft_router = APIRouter() 10 | 11 | @nft_router.post("/frontend_upload") 12 | async def frontend_upload( 13 | imageBase64: str = Form(...), 14 | name: str = Form(...), 15 | access_token: str = Form(...), 16 | art_type: str = Form(...), 17 | description: str = Form(...), 18 | price: float = Form(...) 19 | ): 20 | # Step 1: Upload image 21 | upload_result, upload_error = await upload_image_to_api(imageBase64, name) 22 | if upload_error: 23 | return upload_error 24 | if "error" in upload_result: 25 | return JSONResponse(content={"upload_result": upload_result}, status_code=400) 26 | 27 | # Step 2: Get user info 28 | user_data, user_error = await get_user_info_from_api(access_token) 29 | if user_error: 30 | return JSONResponse( 31 | content={ 32 | "error": user_error.body.decode() if hasattr(user_error, "body") else str(user_error), 33 | "upload_result": upload_result 34 | }, 35 | status_code=502 36 | ) 37 | 38 | user_id = user_data.get("id") 39 | if not user_id: 40 | return JSONResponse(content={ 41 | "error": "Could not retrieve user ID from user API response.", 42 | "user_api_response": user_data, 43 | "upload_result": upload_result 44 | }, status_code=500) 45 | 46 | # Step 3: Find the uploaded NFT 47 | nft = await nft_collection.find_one({"name": name, "imageBase64": imageBase64}) 48 | if not nft: 49 | nft = await nft_collection.find_one({"name": name}) 50 | if not nft: 51 | return JSONResponse(content={ 52 | "error": "Could not find the uploaded NFT in MongoDB.", 53 | "user_id": user_id, 54 | "upload_result": upload_result 55 | }, status_code=404) 56 | 57 | image_id = str(nft.get("_id")) 58 | 59 | # Step 4: Update the NFT with owner info and additional fields 60 | update_result = await nft_collection.update_one( 61 | {"_id": nft["_id"]}, 62 | { 63 | "$set": { 64 | "art_type": art_type, 65 | "nft_owner": user_id, 66 | "description": description, 67 | "price": price 68 | } 69 | } 70 | ) 71 | 72 | return { 73 | "message": "NFT saved, user validated, and metadata stored", 74 | "user_id": user_id, 75 | "image_id": image_id, 76 | "description": description, 77 | "art_type": art_type, 78 | "price": price, 79 | "upload_result": upload_result, 80 | "image_name": name, 81 | "mongo_update": { 82 | "matched_count": update_result.matched_count, 83 | "modified_count": update_result.modified_count 84 | } 85 | } 86 | 87 | @nft_router.get("/all", summary="Get all NFTs, optionally filter by art_type") 88 | async def get_all_nfts( 89 | art_type: Optional[str] = Query( 90 | None, 91 | description="NFT art type: digital_art or photography", 92 | regex="^(digital_art|photography)$" 93 | ) 94 | ): 95 | query = {} 96 | if art_type: 97 | query["art_type"] = art_type 98 | 99 | projection = { 100 | "_id": 1, 101 | "name": 1, 102 | "imageBase64": 1, 103 | "description": 1, 104 | "nft_owner": 1, 105 | "price": 1 106 | } 107 | 108 | nfts = [] 109 | async for nft in nft_collection.find(query, projection): 110 | nft["_id"] = str(nft["_id"]) 111 | nfts.append(nft) 112 | 113 | return {"count": len(nfts), "nfts": nfts} -------------------------------------------------------------------------------- /app/nft/utils.py: -------------------------------------------------------------------------------- 1 | from fastapi.responses import JSONResponse 2 | import httpx 3 | 4 | UPLOAD_API_URL = "https://pixora-nft-copyrights-7e3a5bcac7e4.herokuapp.com/upload" 5 | USER_API_URL = "https://pixora-f96ef5c321f5.herokuapp.com/api/user/me" 6 | 7 | async def upload_image_to_api(imageBase64: str, name: str): 8 | json_payload = { 9 | "imageBase64": imageBase64, 10 | "name": name 11 | } 12 | try: 13 | async with httpx.AsyncClient(timeout=20) as client: 14 | upload_response = await client.post(UPLOAD_API_URL, json=json_payload) 15 | upload_result = upload_response.json() 16 | return upload_result, None 17 | except Exception as e: 18 | return None, JSONResponse( 19 | content={"error": f"Upload API connection failed: {str(e)}"}, 20 | status_code=502 21 | ) 22 | 23 | async def get_user_info_from_api(access_token: str): 24 | headers = {"Authorization": f"Bearer {access_token}"} 25 | try: 26 | async with httpx.AsyncClient(timeout=20) as client: 27 | user_response = await client.get(USER_API_URL, headers=headers, follow_redirects=True) 28 | user_response.raise_for_status() 29 | user_data = user_response.json() 30 | return user_data, None 31 | except Exception as e: 32 | return None, JSONResponse( 33 | content={"error": f"User API connection failed: {str(e)}"}, 34 | status_code=502 35 | ) -------------------------------------------------------------------------------- /app/user/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, Field, HttpUrl, validator 2 | from typing import Optional 3 | from enum import Enum 4 | from datetime import datetime,date 5 | 6 | 7 | class UserRole(str, Enum): 8 | REGULAR = "regular" 9 | ARTIST = "artist" 10 | ADMIN = "admin" 11 | 12 | 13 | class SocialMediaLinks(BaseModel): 14 | twitter: Optional[HttpUrl] = None 15 | instagram: Optional[HttpUrl] = None 16 | facebook: Optional[HttpUrl] = None 17 | website: Optional[HttpUrl] = None 18 | other: Optional[HttpUrl] = None 19 | 20 | 21 | class UserProfileUpdate(BaseModel): 22 | full_name: Optional[str] = Field(None, min_length=2, max_length=100) 23 | bio: Optional[str] = Field(None, max_length=500) 24 | profile_image_url: Optional[HttpUrl] = None 25 | cover_image_url: Optional[HttpUrl] = None 26 | social_media: Optional[SocialMediaLinks] = None 27 | notification_preferences: Optional[dict] = None 28 | 29 | class Config: 30 | schema_extra = { 31 | "example": { 32 | "full_name": "John Doe", 33 | "bio": "Digital artist specializing in abstract art", 34 | "profile_image_url": "https://example.com/profile.jpg", 35 | "cover_image_url": "https://example.com/cover.jpg", 36 | "social_media": { 37 | "twitter": "https://twitter.com/johndoe", 38 | "instagram": "https://instagram.com/johndoe" 39 | }, 40 | "notification_preferences": { 41 | "email_notifications": True, 42 | "sale_alerts": True, 43 | "auction_updates": True 44 | } 45 | } 46 | } 47 | 48 | 49 | class UserPublicProfile(BaseModel): 50 | id: str 51 | username: str 52 | full_name: str 53 | bio: Optional[str] = None 54 | profile_image_url: Optional[HttpUrl] = None 55 | cover_image_url: Optional[HttpUrl] = None 56 | role: UserRole 57 | is_verified_artist: bool 58 | social_media: Optional[SocialMediaLinks] = None 59 | created_at: datetime 60 | artworks_count: int = 0 61 | 62 | class Config: 63 | schema_extra = { 64 | "example": { 65 | "id": "user123", 66 | "username": "johndoe", 67 | "full_name": "John Doe", 68 | "bio": "Digital artist specializing in abstract art", 69 | "profile_image_url": "https://example.com/profile.jpg", 70 | "cover_image_url": "https://example.com/cover.jpg", 71 | "role": "artist", 72 | "is_verified_artist": True, 73 | "social_media": { 74 | "twitter": "https://twitter.com/johndoe", 75 | "instagram": "https://instagram.com/johndoe" 76 | }, 77 | "created_at": "2025-01-01T00:00:00Z", 78 | "artworks_count": 15 79 | } 80 | } 81 | 82 | 83 | class UserPrivateProfile(UserPublicProfile): 84 | email: EmailStr 85 | notification_preferences: Optional[dict] = None 86 | wallet_address: Optional[str] = None 87 | 88 | class Config: 89 | schema_extra = { 90 | "example": { 91 | "id": "user123", 92 | "username": "johndoe", 93 | "full_name": "John Doe", 94 | "email": "john.doe@example.com", 95 | "bio": "Digital artist specializing in abstract art", 96 | "profile_image_url": "https://example.com/profile.jpg", 97 | "cover_image_url": "https://example.com/cover.jpg", 98 | "role": "artist", 99 | "is_verified_artist": True, 100 | "social_media": { 101 | "twitter": "https://twitter.com/johndoe", 102 | "instagram": "https://instagram.com/johndoe" 103 | }, 104 | "created_at": "2025-01-01T00:00:00Z", 105 | "artworks_count": 15, 106 | "notification_preferences": { 107 | "email_notifications": True, 108 | "sale_alerts": True, 109 | "auction_updates": True 110 | }, 111 | "wallet_address": "0x1234567890abcdef1234567890abcdef12345678" 112 | } 113 | } 114 | 115 | 116 | 117 | # Define the verification status as an enum for better type safety 118 | class VerificationStatus(str, Enum): 119 | PENDING = "pending" 120 | APPROVED = "approved" 121 | REJECTED = "rejected" 122 | 123 | 124 | # Input model - what user submits when creating a verification request 125 | class VerificationRequestInput(BaseModel): 126 | address: str = Field(..., description="User's physical address") 127 | id_front_image: str = Field(..., description="Base64 encoded image of ID front") 128 | id_back_image: str = Field(..., description="Base64 encoded image of ID back") 129 | about_user_article_link: HttpUrl = Field(..., description="Link to an article about the user") 130 | 131 | class Config: 132 | json_schema_extra = { 133 | "example": { 134 | "address": "123 Main St, City, Country", 135 | "id_front_image": "base64_encoded_string...", 136 | "id_back_image": "base64_encoded_string...", 137 | "about_user_article_link": "https://example.com/about-user" 138 | } 139 | } 140 | 141 | 142 | # Database model - the complete verification request as stored in DB 143 | class VerificationRequestInDB(BaseModel): 144 | id: Optional[str] = Field(None, alias="_id", description="Verification request ID") 145 | user_id: str = Field(..., description="ID of the user who submitted the request") 146 | user_email: str = Field(..., description="Email of the user") 147 | user_name: str = Field(..., description="Full name of the user") 148 | address: str = Field(..., description="User's physical address") 149 | id_front_image: str = Field(..., description="Base64 encoded image of ID front") 150 | id_back_image: str = Field(..., description="Base64 encoded image of ID back") 151 | about_user_article_link: HttpUrl = Field(..., description="Link to an article about the user") 152 | status: VerificationStatus = Field(default=VerificationStatus.PENDING, 153 | description="Current status of the verification request") 154 | request_date: datetime = Field(default_factory=datetime.utcnow, description="Date when request was submitted") 155 | 156 | class Config: 157 | populate_by_name = True 158 | arbitrary_types_allowed = True 159 | json_encoders = { 160 | datetime: lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S") 161 | } 162 | 163 | 164 | # Response model - what users see when getting verification requests 165 | class VerificationRequestResponse(BaseModel): 166 | id: str = Field(..., description="Verification request ID") 167 | user_id: str = Field(..., description="ID of the user who submitted the request") 168 | user_email: str = Field(..., description="Email of the user") 169 | user_name: str = Field(..., description="Full name of the user") 170 | address: str = Field(..., description="User's physical address") 171 | id_front_image: str = Field(..., description="Base64 encoded image of ID front") 172 | id_back_image: str = Field(..., description="Base64 encoded image of ID back") 173 | about_user_article_link: HttpUrl = Field(..., description="Link to an article about the user") 174 | status: VerificationStatus = Field(..., description="Current status of the verification request") 175 | request_date: str = Field(..., description="Formatted date when request was submitted") 176 | 177 | class Config: 178 | json_schema_extra = { 179 | "example": { 180 | "id": "68298f838b7e3d09bb3babcb", 181 | "user_id": "6809644fd457793bad901156", 182 | "user_email": "john.doe@example.com", 183 | "user_name": "John Doe", 184 | "address": "123 Main St, City, Country", 185 | "id_front_image": "base64_encoded_string...", 186 | "id_back_image": "base64_encoded_string...", 187 | "about_user_article_link": "https://example.com/about-user", 188 | "status": "pending", 189 | "request_date": "2025-05-18 07:57:10" 190 | } 191 | } 192 | 193 | 194 | 195 | class UserDetails(BaseModel): 196 | id: str 197 | first_name: str 198 | last_name: str 199 | email: EmailStr 200 | contact: str 201 | birthday: date 202 | profile_image: str 203 | cover_image: str 204 | user_type: str 205 | bio: str 206 | facebook: str 207 | instagram: str 208 | twitter: str 209 | linkedin: str 210 | verification_status: str 211 | created_at: datetime 212 | updated_at: datetime 213 | 214 | class Config: 215 | schema_extra = { 216 | "example": { 217 | "id": "6809644fd457793bad901156", 218 | "first_name": "John", 219 | "last_name": "Doe", 220 | "email": "john.doe@example.com", 221 | "contact": "+1234567890", 222 | "birthday": "1990-01-01", 223 | "profile_image": "/9j/4AAQSkZJRgABAQAAAQABAAD/...", 224 | "cover_image": "/9j/4AAQSkZJRgABAQAAAQABAAD/...", 225 | "user_type": "artist", 226 | "bio": "Digital artist specializing in abstract art", 227 | "facebook": "www.facebook.com/johndoe", 228 | "instagram": "www.instagram.com/johndoe", 229 | "twitter": "www.twitter.com/johndoe", 230 | "linkedin": "www.linkedin.com/in/johndoe", 231 | "verification_status": "verified", 232 | "created_at": "2025-04-23T22:06:07.154+00:00", 233 | "updated_at": "2025-04-23T22:06:07.154+00:00" 234 | } 235 | } 236 | 237 | 238 | class UpdateUserProfile(BaseModel): 239 | first_name: str 240 | last_name: str 241 | contact: str 242 | profile_image: str 243 | cover_image: str 244 | bio: str 245 | facebook: str 246 | instagram: str 247 | twitter: str 248 | linkedin: str 249 | 250 | class Config: 251 | schema_extra = { 252 | "example": { 253 | "first_name": "Jane", 254 | "last_name": "Doe", 255 | "contact": "+1234567890", 256 | "profile_image": "/9j/4AAQSkZJRgABAQAAAQABAAD/...", 257 | "cover_image": "/9j/4AAQSkZJRgABAQAAAQABAAD/...", 258 | "bio": "Digital artist specializing in abstract art", 259 | "facebook": "www.facebook.com/johndoe", 260 | "instagram": "www.instagram.com/johndoe", 261 | "twitter": "www.twitter.com/johndoe", 262 | "linkedin": "www.linkedin.com/in/johndoe", 263 | } 264 | } -------------------------------------------------------------------------------- /app/user/routes.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | from fastapi import APIRouter, Depends, HTTPException, status, Body 3 | from datetime import datetime 4 | from app.auth.jwt_handler import get_current_user 5 | from app.user.models import VerificationRequestInput, UpdateUserProfile 6 | from app.user.utils import user_helper, user_details_helper 7 | from app.database import get_db 8 | 9 | user_router = APIRouter() 10 | 11 | 12 | @user_router.get("/me", response_model=dict) 13 | async def get_user_details(current_user: dict = Depends(get_current_user)): 14 | """ 15 | Get details of the currently authenticated user 16 | """ 17 | return user_helper(current_user) 18 | 19 | 20 | @user_router.get("/{user_id}", response_model=dict) 21 | async def get_user(user_id: str, current_user: dict = Depends(get_current_user)): 22 | """ 23 | Get details of a specific user by ID (requires authentication) 24 | """ 25 | # Only allow users to view their own profile or implement role-based access control 26 | if current_user["id"] != user_id: 27 | raise HTTPException( 28 | status_code=status.HTTP_403_FORBIDDEN, 29 | detail="Access denied" 30 | ) 31 | 32 | return user_helper(current_user) 33 | 34 | 35 | @user_router.post("/verification-request", response_model=dict) 36 | async def submit_verification_request( 37 | request_data: VerificationRequestInput = Body(...), 38 | current_user: dict = Depends(get_current_user) 39 | ): 40 | """ 41 | Submit a verification request 42 | """ 43 | db = await get_db() 44 | 45 | # Get user ID, checking both possible keys 46 | if "_id" in current_user: 47 | user_id = str(current_user["_id"]) 48 | elif "id" in current_user: 49 | user_id = current_user["id"] 50 | else: 51 | # Print available keys for debugging 52 | print(f"Available keys in current_user: {current_user.keys()}") 53 | raise HTTPException( 54 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 55 | detail="Could not determine user ID" 56 | ) 57 | 58 | # Check if there's a pending verification request 59 | existing_request = await db["VerificationRequests"].find_one({ 60 | "user_id": user_id, 61 | "status": "pending" 62 | }) 63 | 64 | if existing_request: 65 | raise HTTPException( 66 | status_code=status.HTTP_400_BAD_REQUEST, 67 | detail="You already have a pending verification request" 68 | ) 69 | 70 | # Create verification request 71 | verification_request = { 72 | "user_id": user_id, 73 | "user_email": current_user.get("email", ""), 74 | "user_name": f"{current_user.get('first_name', '')} {current_user.get('last_name', '')}", 75 | "address": request_data.address, 76 | "id_front_image": request_data.id_front_image, 77 | "id_back_image": request_data.id_back_image, 78 | "about_user_article_link": request_data.about_user_article_link, 79 | "status": "pending", 80 | "request_date": datetime.utcnow(), 81 | } 82 | 83 | # Insert verification request into database 84 | result = await db["VerificationRequests"].insert_one(verification_request) 85 | 86 | return { 87 | "message": "Verification request submitted successfully", 88 | "request_id": str(result.inserted_id) 89 | } 90 | 91 | 92 | @user_router.get("/verification-requests", response_description="Get all verification requests for the current user") 93 | async def get_verification_requests(current_user: dict = Depends(get_current_user)): 94 | """ 95 | Get all verification requests for the current user 96 | """ 97 | # Get the database connection 98 | db = await get_db() 99 | 100 | # Get user ID from the current user (as string) 101 | user_id = current_user["id"] 102 | 103 | print(f"Current user: {current_user}") 104 | print(f"Looking for verification requests with user_id: {user_id}") 105 | 106 | # First check if there are ANY verification requests in the collection 107 | all_requests_count = await db["VerificationRequests"].count_documents({}) 108 | print(f"Total documents in VerificationRequests collection: {all_requests_count}") 109 | 110 | # Try to find ALL verification requests first 111 | all_requests = [] 112 | async for doc in db["VerificationRequests"].find().limit(5): 113 | print(f"Sample document: {doc}") 114 | if "user_id" in doc: 115 | print(f"Sample user_id: {doc['user_id']} (type: {type(doc['user_id']).__name__})") 116 | all_requests.append(doc) 117 | 118 | print(f"Sample of {len(all_requests)} documents from collection") 119 | 120 | # Now try different ways to find documents for this specific user 121 | 122 | # 1. Try exact match 123 | cursor = db["VerificationRequests"].find({"user_id": user_id}) 124 | 125 | verification_requests = [] 126 | async for request in cursor: 127 | # Keep a copy of the original document for inspection 128 | request_copy = dict(request) 129 | 130 | # Add a string id for API response 131 | request_copy["id"] = str(request["_id"]) 132 | 133 | # Format date if needed 134 | if "request_date" in request_copy and isinstance(request_copy["request_date"], datetime): 135 | request_copy["request_date"] = request_copy["request_date"].strftime("%Y-%m-%d %H:%M:%S") 136 | 137 | verification_requests.append(request_copy) 138 | 139 | print(f"Found {len(verification_requests)} verification requests for user_id: {user_id}") 140 | 141 | # If nothing found, try a more flexible approach 142 | if not verification_requests and all_requests: 143 | print("Trying more flexible match...") 144 | # This is a diagnostic step to see if we can find the user's requests with different approaches 145 | for doc in all_requests: 146 | if "user_id" in doc and doc["user_id"] == user_id: 147 | print(f"Found match with exact string comparison!") 148 | elif "user_id" in doc and str(doc["user_id"]) == user_id: 149 | print(f"Found match after converting to string!") 150 | 151 | # Return whatever we found 152 | return verification_requests 153 | 154 | 155 | @user_router.get("/users/me", response_model=dict) 156 | async def get_logged_in_user_details(current_user: dict = Depends(get_current_user)): 157 | """ 158 | Fetch the logged-in user's details 159 | """ 160 | return user_details_helper(current_user) 161 | 162 | @user_router.put("/me/profile", response_model=dict) 163 | async def update_user_profile( 164 | update_data: UpdateUserProfile = Body(...), 165 | current_user: dict = Depends(get_current_user) 166 | ): 167 | """ 168 | Update the first name, last name, and bio of the currently authenticated user 169 | """ 170 | db = await get_db() 171 | 172 | # Update the user's first_name, last_name, and bio in the database 173 | result = await db["users"].update_one( 174 | {"_id": current_user["id"]}, # Filter by user ID 175 | { 176 | "$set": { 177 | "first_name": update_data.first_name, 178 | "last_name": update_data.last_name, 179 | "contact": update_data.contact, 180 | "profile_image": update_data.profile_image, 181 | "cover_image": update_data.cover_image, 182 | "bio": update_data.bio, 183 | "facebook": update_data.facebook, 184 | "instagram": update_data.instagram, 185 | "twitter": update_data.twitter, 186 | "linkedin": update_data.linkedin 187 | } 188 | } 189 | ) 190 | 191 | if result.modified_count == 0: 192 | raise HTTPException( 193 | status_code=status.HTTP_400_BAD_REQUEST, 194 | detail="Failed to update the user profile" 195 | ) 196 | 197 | # Return the updated fields 198 | return { 199 | "message": "Profile updated successfully" 200 | } -------------------------------------------------------------------------------- /app/user/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from bson import ObjectId 3 | 4 | 5 | def user_helper(user) -> dict: 6 | """ 7 | Convert MongoDB user document to a dict with properly formatted fields 8 | """ 9 | user_id = str(user["_id"]) if "_id" in user else user.get("id") 10 | 11 | return { 12 | "id": user_id, 13 | "first_name": user["first_name"], 14 | "last_name": user["last_name"], 15 | "email": user["email"], 16 | "contact": user["contact"], 17 | "birthday": user["birthday"], 18 | "created_at": user["created_at"], 19 | "updated_at": user["updated_at"] 20 | } 21 | 22 | 23 | def parse_user_from_db(user_data): 24 | """ 25 | Parse user data from database to proper format 26 | """ 27 | if not user_data: 28 | return None 29 | 30 | user_data["id"] = str(user_data["_id"]) 31 | del user_data["_id"] 32 | 33 | if "password" in user_data: 34 | del user_data["password"] 35 | 36 | return user_data 37 | 38 | 39 | def verification_request_helper(request) -> dict: 40 | """ 41 | Format verification request data 42 | """ 43 | return { 44 | "verification_id": str(request["_id"]), 45 | "user_id": request["user_id"], 46 | "address": request["address"], 47 | "id_front_image": request["id_front_image"], 48 | "id_back_image": request["id_back_image"], 49 | "about_user_article_link": request["about_user_article_link"], 50 | "status": request["status"], 51 | "request_date": request["request_date"].strftime("%Y-%m-%d %H:%M:%S") 52 | if isinstance(request["request_date"], datetime.datetime) 53 | else request["request_date"], 54 | } 55 | 56 | 57 | 58 | def user_details_helper(user) -> dict: 59 | """ 60 | Format user details for response 61 | """ 62 | return { 63 | "id": str(user["id"]), 64 | "first_name": user.get("first_name", ""), 65 | "last_name": user.get("last_name", ""), 66 | "email": user.get("email", ""), 67 | "contact": user.get("contact", ""), 68 | "birthday": user.get("birthday", ""), 69 | "profile_image": user.get("profile_image", ""), 70 | "cover_image": user.get("cover_image", ""), 71 | "user_type": user.get("user_type", ""), 72 | "bio": user.get("bio", ""), 73 | "facebook": user.get("facebook", ""), 74 | "instagram": user.get("instagram", ""), 75 | "twitter": user.get("twitter", ""), 76 | "linkedin": user.get("linkedin", ""), 77 | "verification_status": user.get("verification_status", ""), 78 | "created_at": user.get("created_at", ""), 79 | "updated_at": user.get("updated_at", "") 80 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from fastapi import FastAPI 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | from app.admin.routes import admin_router 7 | from app.auth.routes import auth_router 8 | from app.nft.routes import nft_router 9 | from app.user.routes import user_router 10 | from app.database import init_db, db 11 | 12 | app = FastAPI(title="pixora API", 13 | description="Blockchain-based Photos/Digital Art publishing, buying & selling platform") 14 | 15 | #CORS middleware 16 | app.add_middleware( 17 | CORSMiddleware, 18 | allow_origins=["*"], # For development, you can use ["*"] to allow all origins 19 | allow_credentials=True, 20 | allow_methods=["*"], # Allow all HTTP methods 21 | allow_headers=["*"], # Allow all headers 22 | ) 23 | 24 | @asynccontextmanager 25 | async def lifespan(app: FastAPI): 26 | # Code to run on startup 27 | await init_db() 28 | yield 29 | 30 | app.include_router(auth_router, prefix="/api/auth", tags=["Authentication"]) 31 | app.include_router(user_router, prefix="/api/user", tags=["Users"]) 32 | app.include_router(nft_router, prefix="/api/nft", tags=["NFTs"]) 33 | 34 | app.include_router(admin_router, prefix="/api/admin", tags=["Admin"]) 35 | 36 | @app.get("/") 37 | def read_root(): 38 | return {"Pixora FastAPI"} 39 | 40 | @app.get("/health", tags=["Health"]) 41 | async def health_check(): 42 | try: 43 | # Check if MongoDB is connected 44 | await db.client.admin.command('ping') 45 | return {"status": "healthy", "database": "connected"} 46 | except Exception as e: 47 | return {"status": "unhealthy", "database": str(e)} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi~=0.115.11 2 | uvicorn==0.23.2 3 | python-jose~=3.4.0 4 | passlib==1.7.4 5 | pydantic~=2.11.3 6 | python-multipart==0.0.6 7 | python-dotenv==1.0.0 8 | email-validator==2.0.0 9 | pymongo==4.2.0 10 | motor==3.1.1 11 | bcrypt 12 | httpx~=0.28.1 13 | jose~=1.0.0 --------------------------------------------------------------------------------