├── Chapter02 ├── Chapter02-Aggregation_Frameworks.js ├── Chapter02-Creating-new-Documents.js ├── Chapter02-Deleting-Documents.js ├── Chapter02-Projection.js ├── Chapter02-Querying-in-MongoDB.js └── Chapter02-Updating-Documents.js ├── Chapter03 ├── .env ├── chapter3_01.py ├── chapter3_02.py ├── chapter3_03.py ├── chapter3_04.py ├── chapter3_05.py ├── chapter3_06.py ├── chapter3_07.py ├── chapter3_08.py ├── chapter3_09.py ├── chapter3_10.py ├── chapter3_11.py ├── chapter3_12.py ├── chapter3_13.py ├── chapter3_14.py ├── chapter3_15.py ├── chapter3_16.py ├── chapter3_17.py └── requirements.txt ├── Chapter04 ├── car.jpeg ├── chapter4_01.py ├── chapter4_02.py ├── chapter4_03.py ├── chapter4_04.py ├── chapter4_05.py ├── chapter4_06.py ├── chapter4_07.py ├── chapter4_08.py ├── chapter4_09.py ├── chapter4_10.py ├── chapter4_11.py ├── chapter4_12.py ├── chapter4_13.py ├── chapter4_14.py ├── chapter4_15.py ├── chapter4_16.py ├── chapter4_17.py ├── requirements.txt └── routers │ ├── __pycache__ │ ├── cars.cpython-311.pyc │ └── user.cpython-311.pyc │ ├── cars.py │ └── user.py ├── Chapter05 └── frontend │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── vite.svg │ ├── src │ ├── App.jsx │ ├── App2.jsx │ ├── App3.jsx │ ├── components │ │ ├── Button.jsx │ │ ├── Card.jsx │ │ └── Header.jsx │ ├── index.css │ └── main.jsx │ ├── tailwind.config.js │ └── vite.config.js ├── Chapter06 ├── backend │ ├── .gitignore │ ├── README.txt │ ├── app.py │ ├── authentication.py │ ├── models.py │ ├── requirements.txt │ ├── routers │ │ ├── __init__.py │ │ └── users.py │ └── users.json └── frontend │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── vite.svg │ ├── src │ ├── App.jsx │ ├── AuthContext.jsx │ ├── Login.jsx │ ├── Message.jsx │ ├── Register.jsx │ ├── Users.jsx │ ├── index.css │ └── main.jsx │ ├── tailwind.config.js │ └── vite.config.js ├── Chapter07 ├── .env └── backend │ ├── .gitignore │ ├── app.py │ ├── authentication.py │ ├── config.py │ ├── models.py │ ├── requirements.txt │ ├── routers │ ├── __init__.py │ ├── cars.py │ └── users.py │ └── test_models.py ├── Chapter08 ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.jsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── AuthRequired.jsx │ │ ├── CarCard.jsx │ │ ├── CarForm.jsx │ │ ├── InputField.jsx │ │ └── LoginForm.jsx │ ├── contexts │ │ └── AuthContext.jsx │ ├── hooks │ │ └── useAuth.jsx │ ├── index.css │ ├── layouts │ │ └── RootLayout.jsx │ ├── main.jsx │ ├── pages │ │ ├── Cars.jsx │ │ ├── Home.jsx │ │ ├── Login.jsx │ │ ├── NewCar.jsx │ │ ├── NotFound.jsx │ │ └── SingleCar.jsx │ └── utils │ │ └── fetchCarData.js ├── tailwind.config.js └── vite.config.js ├── Chapter09 ├── .gitignore ├── app.py ├── authentication.py ├── background.py ├── config.py ├── database.py ├── models.py ├── requirements.txt └── routers │ ├── cars.py │ └── user.py ├── Chapter10 ├── .eslintrc.json ├── .gitignore ├── README.md ├── jsconfig.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── next.svg │ └── vercel.svg ├── sample.json ├── src │ ├── actions.js │ ├── app │ │ ├── cars │ │ │ ├── [id] │ │ │ │ └── page.js │ │ │ ├── error.js │ │ │ ├── layout.js │ │ │ └── page.js │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.js │ │ ├── login │ │ │ └── page.js │ │ ├── not-found.js │ │ ├── page.js │ │ └── private │ │ │ └── page.js │ ├── components │ │ ├── CarForm.js │ │ ├── InputField.js │ │ ├── LoginForm.js │ │ ├── LogoutForm.js │ │ └── Navbar.js │ └── lib.js └── tailwind.config.js ├── LICENSE └── README.md /Chapter02/Chapter02-Aggregation_Frameworks.js: -------------------------------------------------------------------------------- 1 | db.movies.aggregate([{$match: {"genres": "Comedy"}}]) 2 | 3 | db.movies.aggregate([ 4 | { 5 | $match: { 6 | type: "movie", 7 | genres: "Comedy" 8 | } 9 | }, 10 | { 11 | $group: { 12 | _id: null, 13 | averageRuntime: { $avg: "$runtime" } 14 | } 15 | } 16 | ]) 17 | -------------------------------------------------------------------------------- /Chapter02/Chapter02-Creating-new-Documents.js: -------------------------------------------------------------------------------- 1 | db.movies.insertOne({ 2 | title: "Once upon a time on Moon", 3 | genres: ["Test"], 4 | year: 2024 5 | }) 6 | 7 | 8 | db.movies.insertMany([ 9 | { 10 | title: "Once upon a time on Moon", 11 | genres: ["Test"], 12 | year: 2024 13 | }, 14 | { 15 | title: "Once upon a time on Mars", 16 | genres: ["Test"], 17 | year: 2023 18 | }, 19 | { 20 | title: "Tiger Force in Paradise", 21 | genres: ["Test"], 22 | year: 2019, 23 | rating: "G" 24 | } 25 | ]) 26 | 27 | -------------------------------------------------------------------------------- /Chapter02/Chapter02-Deleting-Documents.js: -------------------------------------------------------------------------------- 1 | db.movies.deleteMany({genres: "PlaceHolder"}) 2 | 3 | -------------------------------------------------------------------------------- /Chapter02/Chapter02-Projection.js: -------------------------------------------------------------------------------- 1 | db.movies.find( 2 | { 3 | "year": { $gt: 1945 }, 4 | "countries": "USA", 5 | "genres": "Comedy" 6 | }, 7 | { 8 | "_id": 0, 9 | "title": 1, 10 | "countries": 1, 11 | "year": 1 12 | } 13 | ).sort({ "year": 1 }).limit(5) 14 | -------------------------------------------------------------------------------- /Chapter02/Chapter02-Querying-in-MongoDB.js: -------------------------------------------------------------------------------- 1 | db.movies.find() 2 | 3 | db.movies.find({"year": 1969}).limit(5) 4 | 5 | db.movies.countDocuments({"year": 1969}) 6 | 7 | db.movies.find({"year": {$gt: 1945}, "countries": "USA", "genres": "Comedy"}) 8 | 9 | db.movies.countDocuments({"year": {$gt: 1945}, "countries": "USA", "genres": "Comedy"}) 10 | 11 | -------------------------------------------------------------------------------- /Chapter02/Chapter02-Updating-Documents.js: -------------------------------------------------------------------------------- 1 | db.movies.updateOne( 2 | { genres: "Test" }, 3 | { $set: { "genres.$": "PlaceHolder" } } 4 | ) 5 | 6 | 7 | db.movies.updateMany( 8 | { "genres": "Test" }, 9 | { 10 | $set: { "genres.$": "PlaceHolder" }, 11 | $inc: { "year": 1 } 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /Chapter03/.env: -------------------------------------------------------------------------------- 1 | API_URL=https://api.com/v2 2 | SECRET_KEY=s3cretstr1n6 3 | -------------------------------------------------------------------------------- /Chapter03/chapter3_01.py: -------------------------------------------------------------------------------- 1 | def print_name_x_times(name: str, times: int) -> None: 2 | for _ in range(times): 3 | print(name) 4 | 5 | 6 | print_name_x_times("John", 4) 7 | 8 | 9 | def count_users(users: list[str]) -> int: 10 | return len(users) 11 | 12 | 13 | print(count_users(["John", "Paul", "George", "Ringo"])) 14 | -------------------------------------------------------------------------------- /Chapter03/chapter3_02.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | 4 | def square_numbers(numbers: List[int]) -> List[int]: 5 | return [number**2 for number in numbers] 6 | 7 | 8 | # Example usage 9 | input_numbers = [1, 2, 3, 4, 5] 10 | squared_numbers = square_numbers(input_numbers) 11 | print(squared_numbers) # Output: [1, 4, 9, 16, 25] 12 | 13 | 14 | # Example usage with Literal 15 | 16 | account_type: Literal["personal", "business"] 17 | account_type = "name" 18 | -------------------------------------------------------------------------------- /Chapter03/chapter3_03.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | def format_datetime(dt: datetime) -> str: 5 | return dt.strftime("%Y-%m-%d %H:%M:%S") 6 | 7 | 8 | now = datetime.now() 9 | print(format_datetime(now)) 10 | -------------------------------------------------------------------------------- /Chapter03/chapter3_04.py: -------------------------------------------------------------------------------- 1 | def get_users(id: int) -> list[dict]: 2 | return [ 3 | {"id": 1, "name": "Alice"}, 4 | {"id": 2, "name": "Bob"}, 5 | {"id": 3, "name": "Charlie"}, 6 | ] 7 | -------------------------------------------------------------------------------- /Chapter03/chapter3_05.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import BaseModel, ValidationError 4 | 5 | 6 | class User(BaseModel): 7 | id: int 8 | username: str 9 | email: str 10 | dob: datetime 11 | 12 | 13 | Pu = User( 14 | id=1, username="freethrow", email="email@gmail.com", dob=datetime(1975, 5, 13) 15 | ) 16 | 17 | 18 | try: 19 | u = User( 20 | id="one", 21 | username="freethrow", 22 | email="email@gmail.com", 23 | dob=datetime(1975, 5, 13), 24 | ) 25 | print(u) 26 | 27 | 28 | except ValidationError as e: 29 | print(e) 30 | -------------------------------------------------------------------------------- /Chapter03/chapter3_06.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | id: int 6 | username: str 7 | email: str 8 | password: str 9 | 10 | 11 | user = User.model_validate( 12 | { 13 | "id": 1, 14 | "username": "freethrow", 15 | "email": "email@gmail.com", 16 | "password": "somesecret", 17 | } 18 | ) 19 | 20 | print(user) 21 | -------------------------------------------------------------------------------- /Chapter03/chapter3_07.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class UserModel(BaseModel): 7 | id: int 8 | username: str 9 | email: str 10 | account: Literal["personal", "business"] | None = None 11 | nickname: str | None = None 12 | 13 | 14 | print(UserModel.model_fields) 15 | -------------------------------------------------------------------------------- /Chapter03/chapter3_08.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | from pydantic import BaseModel, Field 3 | 4 | 5 | class UserModelFields(BaseModel): 6 | id: int = Field(...) 7 | username: str = Field(...) 8 | email: str = Field(...) 9 | account: Literal["personal", "business"] | None = Field(default=None) 10 | nickname: str | None = Field(default=None) 11 | -------------------------------------------------------------------------------- /Chapter03/chapter3_09.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class UserModelFields(BaseModel): 7 | id: int = Field(alias="user_id") 8 | username: str = Field(alias="name") 9 | email: str = Field() 10 | account: Literal["personal", "business"] | None = Field( 11 | default=None, alias="account_type" 12 | ) 13 | nickname: str | None = Field(default=None, alias="nick") 14 | 15 | 16 | external_api_data = { 17 | "user_id": 234, 18 | "name": "Marko", 19 | "email": "email@gmail.com", 20 | "account_type": "personal", 21 | "nick": "freethrow", 22 | } 23 | 24 | user = UserModelFields.model_validate(external_api_data) 25 | -------------------------------------------------------------------------------- /Chapter03/chapter3_10.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import uuid4 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class ChessTournament(BaseModel): 8 | id: int = Field(strict=True) 9 | dt: datetime = Field(default_factory=datetime.now) 10 | name: str = Field(min_length=10, max_length=30) 11 | num_players: int = Field(ge=4, le=16, multiple_of=2) 12 | code: str = Field(default_factory=uuid4) 13 | -------------------------------------------------------------------------------- /Chapter03/chapter3_11.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, Field 2 | 3 | 4 | class UserModel(BaseModel): 5 | id: int = Field() 6 | username: str = Field(min_length=5, max_length=20) 7 | email: EmailStr = Field() 8 | password: str = Field(min_length=5, max_length=20, pattern="^[a-zA-Z0-9]+$") 9 | 10 | 11 | u = UserModel( 12 | id=1, 13 | username="freethrow", 14 | email="email@gmail.com", 15 | password="password123", 16 | ) 17 | 18 | print(u.model_dump()) 19 | 20 | print(u.model_dump_json(exclude=set("password"))) 21 | -------------------------------------------------------------------------------- /Chapter03/chapter3_12.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict, EmailStr, Field 2 | 3 | 4 | class UserModel(BaseModel): 5 | id: int = Field() 6 | username: str = Field(min_length=5, max_length=20, alias="name") 7 | email: EmailStr = Field() 8 | password: str = Field(min_length=5, max_length=20, pattern="^[a-zA-Z0-9]+$") 9 | 10 | model_config = ConfigDict(extra="forbid", populate_by_name=True) 11 | -------------------------------------------------------------------------------- /Chapter03/chapter3_13.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import BaseModel, field_serializer 4 | 5 | 6 | class Account(BaseModel): 7 | balance: float 8 | updated: datetime 9 | 10 | @field_serializer("balance", when_used="always") 11 | def serialize_balance(self, value: float) -> float: 12 | return round(value, 2) 13 | 14 | @field_serializer("updated", when_used="json") 15 | def serialize_updated(self, value: datetime) -> str: 16 | return value.isoformat() 17 | 18 | 19 | account_data = { 20 | "balance": 123.45545, 21 | "updated": datetime.now(), 22 | } 23 | 24 | account = Account.model_validate(account_data) 25 | 26 | print("Python dictionary:", account.model_dump()) 27 | print("JSON:", account.model_dump_json()) 28 | -------------------------------------------------------------------------------- /Chapter03/chapter3_14.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, field_validator 2 | 3 | 4 | class Article(BaseModel): 5 | id: int 6 | title: str 7 | content: str 8 | published: bool 9 | 10 | @field_validator("title") 11 | def check_title(cls, v: str) -> str: 12 | if "FARM stack" not in v: 13 | raise ValueError('Title must contain "FARM stack"') 14 | return v.title() 15 | -------------------------------------------------------------------------------- /Chapter03/chapter3_15.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Self 2 | 3 | from pydantic import BaseModel, EmailStr, model_validator, ValidationError 4 | 5 | 6 | class UserModelV(BaseModel): 7 | id: int 8 | username: str 9 | email: EmailStr 10 | password1: str 11 | password2: str 12 | 13 | @model_validator(mode="after") 14 | def check_passwords_match(self) -> Self: 15 | pw1 = self.password1 16 | pw2 = self.password2 17 | if pw1 is not None and pw2 is not None and pw1 != pw2: 18 | raise ValueError("passwords do not match") 19 | return self 20 | 21 | @model_validator(mode="before") 22 | @classmethod 23 | def check_private_data(cls, data: Any) -> Any: 24 | if isinstance(data, dict): 25 | assert "private_data" not in data, "Private data should not be included" 26 | return data 27 | 28 | 29 | usr_data = { 30 | "id": 1, 31 | "username": "freethrow", 32 | "email": "email@gmail.com", 33 | "password1": "password123", 34 | "password2": "password456", 35 | "private_data": "some private data", 36 | } 37 | 38 | try: 39 | user = UserModelV.model_validate(usr_data) 40 | print(user) 41 | except ValidationError as e: 42 | print(e) 43 | -------------------------------------------------------------------------------- /Chapter03/chapter3_16.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pydantic import BaseModel 3 | 4 | car_data = { 5 | "brand": "Ford", 6 | "models": [ 7 | {"model": "Mustang", "year": 1964}, 8 | {"model": "Focus", "year": 1975}, 9 | {"model": "Explorer", "year": 1999}, 10 | ], 11 | "country": "USA", 12 | } 13 | 14 | 15 | class CarModel(BaseModel): 16 | model: str 17 | year: int 18 | 19 | 20 | class CarBrand(BaseModel): 21 | brand: str 22 | models: List[CarModel] 23 | country: str 24 | -------------------------------------------------------------------------------- /Chapter03/chapter3_17.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | api_url: str = Field(default="") 7 | secret_key: str = Field(default="") 8 | 9 | class Config: 10 | env_file = ".env" 11 | 12 | 13 | print(Settings().model_dump()) 14 | -------------------------------------------------------------------------------- /Chapter03/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter03/requirements.txt -------------------------------------------------------------------------------- /Chapter04/car.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter04/car.jpeg -------------------------------------------------------------------------------- /Chapter04/chapter4_01.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/") 7 | async def root(): 8 | return {"message": "Hello FastAPI"} 9 | 10 | 11 | @app.post("/") 12 | async def post_root(): 13 | return {"message": "Post request success!"} 14 | -------------------------------------------------------------------------------- /Chapter04/chapter4_02.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/car/{id}") 7 | async def root(id): 8 | return {"car_id": id} 9 | 10 | 11 | @app.get("/carh/{id}") 12 | async def hinted_car_id(id: int): 13 | return {"car_id": id} 14 | -------------------------------------------------------------------------------- /Chapter04/chapter4_03.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/user/{id}") 7 | async def user(id: int): 8 | return {"User_id": id} 9 | 10 | 11 | @app.get("/user/me") 12 | async def me_user(): 13 | return {"User_id": "This is me!"} 14 | -------------------------------------------------------------------------------- /Chapter04/chapter4_04.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from fastapi import FastAPI, Path 4 | 5 | app = FastAPI() 6 | 7 | 8 | class AccountType(str, Enum): 9 | FREE = "free" 10 | PRO = "pro" 11 | 12 | 13 | @app.get("/account/{acc_type}/{months}") 14 | async def account(acc_type: AccountType, months: int = Path(..., ge=3, le=12)): 15 | return {"message": "Account created", "account_type": acc_type, "months": months} 16 | -------------------------------------------------------------------------------- /Chapter04/chapter4_05.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/cars/price") 7 | async def cars_by_price(min_price: int = 0, max_price: int = 100000): 8 | return {"Message": f"Listing cars with prices between {min_price} and {max_price}"} 9 | -------------------------------------------------------------------------------- /Chapter04/chapter4_06.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from fastapi import Body, FastAPI 3 | 4 | app = FastAPI() 5 | 6 | 7 | @app.post("/cars") 8 | async def new_car(data: Dict = Body(...)): 9 | print(data) 10 | return {"message": data} 11 | -------------------------------------------------------------------------------- /Chapter04/chapter4_07.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | 4 | 5 | class InsertCar(BaseModel): 6 | brand: str 7 | model: str 8 | year: int 9 | 10 | 11 | app = FastAPI() 12 | 13 | 14 | @app.post("/cars") 15 | async def new_car(data: InsertCar): 16 | print(data) 17 | return {"message": data} 18 | -------------------------------------------------------------------------------- /Chapter04/chapter4_08.py: -------------------------------------------------------------------------------- 1 | from fastapi import Body, FastAPI 2 | from pydantic import BaseModel 3 | 4 | 5 | class InsertCar(BaseModel): 6 | brand: str 7 | model: str 8 | year: int 9 | 10 | 11 | class UserModel(BaseModel): 12 | username: str 13 | name: str 14 | 15 | 16 | app = FastAPI() 17 | 18 | 19 | @app.post("/car/user") 20 | async def new_car_model(car: InsertCar, user: UserModel, code: int = Body(None)): 21 | return {"car": car, "user": user, "code": code} 22 | -------------------------------------------------------------------------------- /Chapter04/chapter4_09.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/cars") 7 | async def raw_request(request: Request): 8 | return {"message": request.base_url, "all": dir(request)} 9 | -------------------------------------------------------------------------------- /Chapter04/chapter4_10.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import FastAPI, Header 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.get("/headers") 9 | async def read_headers(user_agent: Annotated[str | None, Header()] = None): 10 | return {"User-Agent": user_agent} 11 | -------------------------------------------------------------------------------- /Chapter04/chapter4_11.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, File, Form, UploadFile 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.post("/upload") 7 | async def upload( 8 | file: UploadFile = File(...), brand: str = Form(...), model: str = Form(...) 9 | ): 10 | return {"brand": brand, "model": model, "file_name": file.filename} 11 | -------------------------------------------------------------------------------- /Chapter04/chapter4_12.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | from fastapi import FastAPI, File, Form, UploadFile 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.post("/upload") 9 | async def upload( 10 | picture: UploadFile = File(...), brand: str = Form(...), model: str = Form(...) 11 | ): 12 | with open("saved_file.png", "wb") as buffer: 13 | shutil.copyfileobj(picture.file, buffer) 14 | return {"brand": brand, "model": model, "file_name": picture.filename} 15 | -------------------------------------------------------------------------------- /Chapter04/chapter4_13.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, status 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/", status_code=status.HTTP_208_ALREADY_REPORTED) 7 | async def raw_fa_response(): 8 | return {"message": "fastapi response"} 9 | -------------------------------------------------------------------------------- /Chapter04/chapter4_14.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException, status 2 | from pydantic import BaseModel 3 | 4 | app = FastAPI() 5 | 6 | 7 | class InsertCar(BaseModel): 8 | brand: str 9 | model: str 10 | year: int 11 | 12 | 13 | @app.post("/carsmodel") 14 | async def new_car_model(car: InsertCar): 15 | if car.year > 2022: 16 | raise HTTPException( 17 | status.HTTP_406_NOT_ACCEPTABLE, detail="The car doesn’t exist yet!" 18 | ) 19 | return {"message": car} 20 | -------------------------------------------------------------------------------- /Chapter04/chapter4_15.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Depends, FastAPI 3 | 4 | app = FastAPI() 5 | 6 | 7 | async def pagination(q: str | None = None, skip: int = 0, limit: int = 100): 8 | return {"q": q, "skip": skip, "limit": limit} 9 | 10 | 11 | @app.get("/cars/") 12 | async def read_items(commons: Annotated[dict, Depends(pagination)]): 13 | return commons 14 | 15 | 16 | @app.get("/users/") 17 | async def read_users(commons: Annotated[dict, Depends(pagination)]): 18 | return commons 19 | -------------------------------------------------------------------------------- /Chapter04/chapter4_16.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from routers.cars import router as cars_router 4 | from routers.user import router as users_router 5 | 6 | app = FastAPI() 7 | 8 | app.include_router(cars_router, prefix="/cars", tags=["cars"]) 9 | app.include_router(users_router, prefix="/users", tags=["users"]) 10 | -------------------------------------------------------------------------------- /Chapter04/chapter4_17.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | from fastapi import FastAPI, Request 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.middleware("http") 9 | async def add_random_header(request: Request, call_next): 10 | number = randint(1, 10) 11 | response = await call_next(request) 12 | 13 | response.headers["X-Random-Integer "] = str(number) 14 | return response 15 | 16 | 17 | @app.get("/") 18 | async def root(): 19 | return {"message": "Hello World"} 20 | -------------------------------------------------------------------------------- /Chapter04/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter04/requirements.txt -------------------------------------------------------------------------------- /Chapter04/routers/__pycache__/cars.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter04/routers/__pycache__/cars.cpython-311.pyc -------------------------------------------------------------------------------- /Chapter04/routers/__pycache__/user.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter04/routers/__pycache__/user.cpython-311.pyc -------------------------------------------------------------------------------- /Chapter04/routers/cars.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.get("/") 7 | async def get_cars(): 8 | return {"message": "All cars here"} 9 | -------------------------------------------------------------------------------- /Chapter04/routers/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.get("/") 7 | async def get_users(): 8 | return {"message": "All users here"} 9 | -------------------------------------------------------------------------------- /Chapter05/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /Chapter05/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Chapter05/frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /Chapter05/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Chapter05/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.66", 18 | "@types/react-dom": "^18.2.22", 19 | "@vitejs/plugin-react": "^4.2.1", 20 | "autoprefixer": "^10.4.19", 21 | "eslint": "^8.57.0", 22 | "eslint-plugin-react": "^7.34.1", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.6", 25 | "postcss": "^8.4.38", 26 | "tailwindcss": "^3.4.3", 27 | "vite": "^5.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Chapter05/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /Chapter05/frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter05/frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Card from "./components/Card"; 3 | export default function App() { 4 | 5 | const data = [ 6 | { id: 1, name: "Fiat", year: 2023, model: "Panda", price: 12000 }, 7 | { id: 2, name: "Peugeot", year: 2018, model: "308", price: 16000 }, 8 | { id: 3, name: "Ford", year: 2022, model: "Mustang", price: 25000 }, 9 | { id: 4, name: "Renault", year: 2019, model: "Clio", price: 18000 }, 10 | { id: 5, name: "Citroen", year: 2021, model: "C3 Aircross", price: 22000 }, 11 | { id: 6, name: "Toyota", year: 2020, model: "Yaris", price: 15000 }, 12 | { id: 7, name: "Volkswagen", year: 2021, model: "Golf", price: 28000 }, 13 | { id: 8, name: "BMW", year: 2022, model: "M3", price: 45000 }, 14 | { id: 9, name: "Mercedes", year: 2021, model: "A-Class", price: 35000 }, 15 | { id: 10, name: "Audi", year: 2022, model: "A6", price: 40000 } 16 | ] 17 | const [budget, setBudget] = useState(20000) 18 | 19 | return ( 20 |
21 | 22 |
23 |

Your budget is {budget}

24 | 25 | setBudget(e.target.value)} /> 26 |
27 | 28 |
29 | {data.filter((el) => el.price <= budget).map((el) => { 30 | return ( 31 | 32 | ) 33 | } 34 | )} 35 |
36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /Chapter05/frontend/src/App2.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useState } from "react"; 3 | import Card from "./components/Card"; 4 | 5 | 6 | export default function App() { 7 | 8 | const data = [ 9 | { id: 1, name: "Fiat", year: 2023, model: "Panda", price: 12000 }, 10 | { id: 2, name: "Peugeot", year: 2018, model: "308", price: 16000 }, 11 | { id: 3, name: "Ford", year: 2022, model: "Mustang", price: 25000 }, 12 | { id: 4, name: "Renault", year: 2019, model: "Clio", price: 18000 }, 13 | { id: 5, name: "Citroen", year: 2021, model: "C3 Aircross", price: 22000 }, 14 | { id: 6, name: "Toyota", year: 2020, model: "Yaris", price: 15000 }, 15 | { id: 7, name: "Volkswagen", year: 2021, model: "Golf", price: 28000 }, 16 | { id: 8, name: "BMW", year: 2022, model: "M3", price: 45000 }, 17 | { id: 9, name: "Mercedes", year: 2021, model: "A-Class", price: 35000 }, 18 | { id: 10, name: "Audi", year: 2022, model: "A6", price: 40000 } 19 | ] 20 | 21 | const [budget, setBudget] = useState(20000) 22 | 23 | 24 | 25 | return ( 26 |
27 | 28 |
29 |

Your budget is {budget}

30 | 31 | setBudget(e.target.value)} /> 32 |
33 | 34 |
35 | {data.filter((el) => el.price <= budget).map((el) => { 36 | return ( 37 | 38 | ) 39 | } 40 | )} 41 |
42 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /Chapter05/frontend/src/App3.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | export default function App() { 3 | const [users, setUsers] = useState([]); 4 | useEffect(() => { 5 | fetchUsers(); 6 | }, []); 7 | const fetchUsers = () => { 8 | fetch("https://jsonplaceholder.typicode.com/users") 9 | .then((res) => res.json()) 10 | .then((data) => setUsers(data)); 11 | }; 12 | return ( 13 |
15 |

List of users

16 |
17 |
    18 | {users.map((user) => ( 19 |
  1. {user.name}
  2. 20 | ))} 21 |
22 |
23 |
24 | ); 25 | } -------------------------------------------------------------------------------- /Chapter05/frontend/src/components/Button.jsx: -------------------------------------------------------------------------------- 1 | const Button = () => { 2 | 3 | const handleClick = () => { 4 | console.log("click") 5 | } 6 | return ( 7 | 10 | ) 11 | } 12 | export default Button -------------------------------------------------------------------------------- /Chapter05/frontend/src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | const Card = ({ car: { name, year, model, price } }) => { 2 | 3 | 4 | return ( 5 |
6 |

{name}

7 |

{year} - {model}

8 |

${price}

9 |
10 | ) 11 | } 12 | export default Card -------------------------------------------------------------------------------- /Chapter05/frontend/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | const Header = () => { 2 | return ( 3 |
Header
4 | ) 5 | } 6 | export default Header -------------------------------------------------------------------------------- /Chapter05/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /Chapter05/frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App3.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Chapter05/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /Chapter05/frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Chapter06/backend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Environments 3 | .env 4 | .venv 5 | env/ 6 | venv/ 7 | ENV/ 8 | env.bak/ 9 | venv.bak/ 10 | 11 | 12 | *.pyc 13 | *.pyo 14 | *.so 15 | *.zip 16 | *.egg-info 17 | *.dist-info 18 | *.egg 19 | *.egg-link 20 | *.pycache 21 | *.whl 22 | -------------------------------------------------------------------------------- /Chapter06/backend/README.txt: -------------------------------------------------------------------------------- 1 | FastAPI authentication application with fake MongoDB, uses dictionary as a database -------------------------------------------------------------------------------- /Chapter06/backend/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from routers.users import router as users_router 5 | 6 | # define origins 7 | origins = ["*"] 8 | 9 | app = FastAPI() 10 | 11 | # add CORS middleware 12 | app.add_middleware( 13 | CORSMiddleware, 14 | allow_origins=origins, 15 | allow_credentials=True, 16 | allow_methods=["*"], 17 | allow_headers=["*"], 18 | ) 19 | 20 | 21 | # include the routers 22 | app.include_router(users_router, prefix="/users", tags=["users"]) 23 | -------------------------------------------------------------------------------- /Chapter06/backend/authentication.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import jwt 4 | from fastapi import HTTPException, Security 5 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer 6 | from passlib.context import CryptContext 7 | 8 | 9 | class AuthHandler: 10 | """ 11 | Class for handling user authentication tasks. 12 | """ 13 | 14 | security = HTTPBearer() 15 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 16 | secret = "FARMSTACKsecretString" 17 | 18 | def get_password_hash(self, password: str) -> str: 19 | """ 20 | Returns the hashed password using the provided password string. 21 | 22 | Args: 23 | password (str): The password string to be hashed. 24 | 25 | Returns: 26 | str: The hashed password. 27 | """ 28 | return self.pwd_context.hash(password) 29 | 30 | def verify_password(self, plain_password: str, hashed_password: str) -> bool: 31 | """ 32 | Verify the given plain password matches the hashed password. 33 | 34 | Args: 35 | plain_password (str): The plain text password to verify. 36 | hashed_password (str): The hashed password to compare against. 37 | 38 | Returns: 39 | bool: True if the passwords match, False otherwise. 40 | """ 41 | return self.pwd_context.verify(plain_password, hashed_password) 42 | 43 | def encode_token(self, user_id: int, username: str) -> str: 44 | """ 45 | Encode a token for the given user ID and username using JWT with the specified payload and secret key. 46 | 47 | Args: 48 | user_id (int): The ID of the user. 49 | username (str): The username of the user. 50 | 51 | Returns: 52 | str: The encoded JWT token. 53 | """ 54 | payload = { 55 | "exp": datetime.datetime.now(datetime.timezone.utc) 56 | + datetime.timedelta(minutes=30), 57 | "iat": datetime.datetime.now(datetime.timezone.utc), 58 | "sub": {"user_id": user_id, "username": username}, 59 | } 60 | return jwt.encode(payload, self.secret, algorithm="HS256") 61 | 62 | def decode_token(self, token: str): 63 | """ 64 | Decode a JWT token and return the user ID and username if valid. 65 | 66 | Args: 67 | token (str): The JWT token. 68 | 69 | Raises: 70 | HTTPException: If the token is expired, invalid or missing. 71 | 72 | Returns: 73 | dict: The decoded token payload containing the user ID and username. 74 | """ 75 | try: 76 | payload = jwt.decode(token, self.secret, algorithms=["HS256"]) 77 | return payload["sub"] 78 | except jwt.ExpiredSignatureError: 79 | raise HTTPException(status_code=401, detail="Signature has expired") 80 | except jwt.InvalidTokenError: 81 | raise HTTPException(status_code=401, detail="Invalid token") 82 | 83 | def auth_wrapper( 84 | self, auth: HTTPAuthorizationCredentials = Security(security) 85 | ) -> dict: 86 | """ 87 | Wrapper function for FastAPI Security that decodes the JWT token 88 | and returns the user ID and username. 89 | 90 | Args: 91 | auth (HTTPAuthorizationCredentials, optional): The FastAPI 92 | Security authorization credentials. Defaults to Security(security). 93 | 94 | Raises: 95 | HTTPException: If the token is expired, invalid or missing. 96 | 97 | Returns: 98 | dict: The decoded token payload containing the user ID and username. 99 | """ 100 | return self.decode_token(auth.credentials) 101 | -------------------------------------------------------------------------------- /Chapter06/backend/models.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class UserBase(BaseModel): 7 | id: str = Field(...) 8 | username: str = Field(..., min_length=3, max_length=15) 9 | password: str = Field(...) 10 | 11 | 12 | class UserIn(BaseModel): 13 | username: str = Field(..., min_length=3, max_length=15) 14 | password: str = Field(...) 15 | 16 | 17 | class UserOut(BaseModel): 18 | id: str = Field(...) 19 | username: str = Field(..., min_length=3, max_length=15) 20 | 21 | 22 | class UsersList(BaseModel): 23 | users: List[UserOut] 24 | -------------------------------------------------------------------------------- /Chapter06/backend/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter06/backend/requirements.txt -------------------------------------------------------------------------------- /Chapter06/backend/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter06/backend/routers/__init__.py -------------------------------------------------------------------------------- /Chapter06/backend/routers/users.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | 5 | from fastapi import APIRouter, Body, Depends, HTTPException, Request 6 | from fastapi.encoders import jsonable_encoder 7 | from fastapi.responses import JSONResponse 8 | 9 | from authentication import AuthHandler 10 | from models import UserBase, UserIn, UserOut, UsersList 11 | 12 | router = APIRouter() 13 | 14 | # instantiate the Auth Handler 15 | auth_handler = AuthHandler() 16 | 17 | # register user 18 | # validate the data and create a user if the username and the email are valid and available 19 | 20 | 21 | @router.post("/register", response_description="Register user") 22 | async def register(request: Request, newUser: UserIn = Body(...)) -> UserBase: 23 | users = json.loads(open("users.json").read())["users"] 24 | 25 | # check if users listhas a user with the same username 26 | if any(user["username"] == newUser.username for user in users): 27 | raise HTTPException(status_code=409, detail="Username already taken") 28 | 29 | # hash the password before inserting it into MongoDB 30 | newUser.password = auth_handler.get_password_hash(newUser.password) 31 | 32 | # add a uuid to the user 33 | newUser = jsonable_encoder(newUser) 34 | 35 | newUser["id"] = str(uuid.uuid4()) 36 | 37 | # add the user to the users dictionary 38 | users.append(newUser) 39 | 40 | # write the updated users dictionary to the JSON file 41 | with open("users.json", "w") as f: 42 | json.dump({"users": users}, f, indent=4) 43 | 44 | return newUser 45 | 46 | 47 | # post user 48 | @router.post("/login", response_description="Login user") 49 | async def login(request: Request, loginUser: UserIn = Body(...)) -> str: 50 | users = json.loads(open("users.json").read())["users"] 51 | 52 | # find the user by username in the list of dicts 53 | user = next( 54 | (user for user in users if user["username"] == loginUser.username), None 55 | ) 56 | 57 | # check password 58 | if (user is None) or ( 59 | not auth_handler.verify_password(loginUser.password, user["password"]) 60 | ): 61 | raise HTTPException(status_code=401, detail="Invalid username and/or password") 62 | 63 | token = auth_handler.encode_token(str(user["id"]), user["username"]) 64 | 65 | response = JSONResponse(content={"token": token}) 66 | 67 | return response 68 | 69 | 70 | # me route 71 | @router.get("/me", response_description="Logged in user data", response_model=UserOut) 72 | async def me(request: Request, user_data=Depends(auth_handler.auth_wrapper)): 73 | users = json.loads(open("users.json").read())["users"] 74 | 75 | # find the user by username in the list of dicts 76 | currentUser = next( 77 | (user for user in users if user["username"] == user_data["username"]), None 78 | ) 79 | 80 | return currentUser 81 | 82 | 83 | # route for listing all users, needs auth 84 | @router.get("/list", response_description="List all users") 85 | async def list_users(request: Request, user_data=Depends(auth_handler.auth_wrapper)): 86 | users = json.loads(open("users.json").read())["users"] 87 | 88 | return UsersList(users=users) 89 | -------------------------------------------------------------------------------- /Chapter06/backend/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "username": "marko", 5 | "password": "$2b$12$owWXcY5KgI9s6Rdfjcpx7eXaZOMWf8NaxN.SoLJ4h8O.xzFpRqEee", 6 | "id": "45cd212b-71eb-42b4-9d06-a74f2609764b" 7 | }, 8 | { 9 | "username": "darius", 10 | "password": "$2b$12$e5udCabbENuUPsCrtlt/EuVMwzrTHNzKguFvJ0Fjmr1f06PJDuTOK", 11 | "id": "da38188c-5819-4be3-83e0-bc35c1ace748" 12 | }, 13 | { 14 | "username": "kokos", 15 | "password": "$2b$12$lMUgstvxJzohpI4JHRH8fOI/xeHuL8fuKjlvnXHi7RP64NLo0fbSq", 16 | "id": "bdfc5531-e18b-4369-b4a7-8ae36ad6184d" 17 | }, 18 | { 19 | "username": "maksa", 20 | "password": "$2b$12$wI2UpowayBW.bnHvl/agye7QMtlzc2BVr5ke5JwF9Xsa4RsEPjd7S", 21 | "id": "05fb14cd-5b69-4283-bcc2-00843a3520bd" 22 | }, 23 | { 24 | "username": "tanja", 25 | "password": "$2b$12$PwAewPDrL/8g8kmoPLug7.KdKsTjKo5VR0byJgFxalpVmpOq2AaKa", 26 | "id": "6a44715c-9962-4a0b-af5f-63be871ed752" 27 | }, 28 | { 29 | "username": "zorex", 30 | "password": "$2b$12$8W0GOZsZGWspr8pO8aG0ZO4fuawKoRWNih.floxR9ynkkbZ4B4aFe", 31 | "id": "e4a5eec0-656d-4347-9b21-a473ae6b2739" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /Chapter06/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /Chapter06/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Chapter06/frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /Chapter06/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Chapter06/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simpleauth", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.66", 18 | "@types/react-dom": "^18.2.22", 19 | "@vitejs/plugin-react": "^4.2.1", 20 | "autoprefixer": "^10.4.19", 21 | "eslint": "^8.57.0", 22 | "eslint-plugin-react": "^7.34.1", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.6", 25 | "postcss": "^8.4.38", 26 | "tailwindcss": "^3.4.3", 27 | "vite": "^5.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Chapter06/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /Chapter06/frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter06/frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { AuthProvider, useAuth } from './AuthContext'; 3 | 4 | import Register from './Register'; 5 | import Login from './Login'; 6 | import Users from './Users'; 7 | import Message from './Message'; 8 | 9 | const App = () => { 10 | 11 | const [showLogin, setShowLogin] = useState(true) 12 | 13 | return ( 14 |
15 | 16 |

Simple Auth App

17 | 18 | 19 | 20 |
21 | {showLogin ? : } 22 | 23 |
24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default App; -------------------------------------------------------------------------------- /Chapter06/frontend/src/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect } from 'react'; 2 | 3 | const AuthContext = createContext(); 4 | 5 | // eslint-disable-next-line react/prop-types 6 | export const AuthProvider = ({ children }) => { 7 | const [user, setUser] = useState(null); 8 | const [jwt, setJwt] = useState(null); 9 | const [message, setMessage] = useState(null); 10 | 11 | useEffect(() => { 12 | 13 | const storedJwt = localStorage.getItem('jwt'); 14 | if (storedJwt) { 15 | setJwt(storedJwt); 16 | fetch('http://127.0.0.1:8000/users/me', { 17 | headers: { 18 | Authorization: `Bearer ${storedJwt}`, 19 | }, 20 | }) 21 | .then(res => res.json()) 22 | 23 | .then(data => { 24 | 25 | if (data.username) { 26 | setUser({ username: data.username }); 27 | setMessage(`Welcome back, ${data.username}!`); 28 | } 29 | }) 30 | .catch(() => { 31 | localStorage.removeItem('jwt'); 32 | }); 33 | } 34 | }, []); 35 | 36 | const register = async (username, password) => { 37 | 38 | // Example POST request to your /register endpoint 39 | try { 40 | const response = await fetch('http://127.0.0.1:8000/users/register', { 41 | method: 'POST', 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | }, 45 | body: JSON.stringify({ username, password }), 46 | }); 47 | if (response.ok) { 48 | const data = await response.json(); 49 | setMessage(`Registration successful: user ${data.username} created`); 50 | } else { 51 | const data = await response.json(); 52 | setMessage(`Registration failed: ${JSON.stringify(data)}`); 53 | } 54 | } catch (error) { 55 | setMessage(`Registration failed: ${JSON.stringify(error)}`); 56 | } 57 | }; 58 | 59 | 60 | const login = async (username, password) => { 61 | 62 | setJwt(null) 63 | const response = await fetch('http://127.0.0.1:8000/users/login', { 64 | method: 'POST', 65 | headers: { 66 | 'Content-Type': 'application/json', 67 | }, 68 | body: JSON.stringify({ username, password }), 69 | }); 70 | if (response.ok) { 71 | const data = await response.json(); 72 | setJwt(data.token); 73 | localStorage.setItem('jwt', data.token); 74 | setUser({ username }); 75 | setMessage(`Login successful: token ${data.token.slice(0, 10)}..., user ${username}`); 76 | } else { 77 | const data = await response.json(); 78 | setMessage('Login failed: ' + data.detail); 79 | setUser({ username: null }); 80 | } 81 | 82 | }; 83 | 84 | const logout = () => { 85 | setUser(null); 86 | setJwt(''); 87 | localStorage.removeItem('jwt'); 88 | setMessage('Logout successful'); 89 | }; 90 | 91 | return ( 92 | 93 | {children} 94 | 95 | ); 96 | }; 97 | 98 | export const useAuth = () => useContext(AuthContext); -------------------------------------------------------------------------------- /Chapter06/frontend/src/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useAuth } from './AuthContext'; 3 | 4 | const Login = () => { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | const { login } = useAuth(); 8 | 9 | const handleSubmit = (e) => { 10 | e.preventDefault(); 11 | login(username, password); 12 | setUsername(''); 13 | setPassword(''); 14 | }; 15 | 16 | return ( 17 |
18 |
19 | setUsername(e.target.value)} 25 | /> 26 | setPassword(e.target.value)} 32 | /> 33 | 34 |
35 |
36 | ); 37 | }; 38 | export default Login -------------------------------------------------------------------------------- /Chapter06/frontend/src/Message.jsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from "./AuthContext" 2 | const Message = () => { 3 | const { message } = useAuth() 4 | return ( 5 |
6 |

{message}

7 |
8 | ) 9 | } 10 | export default Message -------------------------------------------------------------------------------- /Chapter06/frontend/src/Register.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useAuth } from './AuthContext'; 3 | 4 | const Register = () => { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | const { register } = useAuth(); 8 | 9 | const handleSubmit = (e) => { 10 | e.preventDefault(); 11 | register(username, password) 12 | setUsername('') 13 | setPassword('') 14 | }; 15 | 16 | return ( 17 |
18 |
19 | setUsername(e.target.value)} 25 | /> 26 | setPassword(e.target.value)} 32 | /> 33 | 34 |
35 |
36 | ); 37 | }; 38 | export default Register -------------------------------------------------------------------------------- /Chapter06/frontend/src/Users.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useAuth } from './AuthContext'; 3 | 4 | const Users = () => { 5 | const { jwt, logout } = useAuth(); 6 | const [users, setUsers] = useState(null); 7 | const [error, setError] = useState(null); 8 | 9 | useEffect(() => { 10 | const fetchUsers = async () => { 11 | const response = await fetch('http://127.0.0.1:8000/users/list', { 12 | headers: { 13 | Authorization: `Bearer ${jwt}`, 14 | }, 15 | }); 16 | const data = await response.json(); 17 | if (!response.ok) { 18 | setError(data.detail); 19 | } 20 | setUsers(data.users); 21 | }; 22 | 23 | if (jwt) { 24 | fetchUsers(); 25 | } 26 | }, [jwt]); 27 | 28 | if (!jwt) return
Please log in to see all the users
; 29 | 30 | return ( 31 |
32 | {users ? ( 33 |
34 |

The list of users

35 |
    36 | {users.map((user) => ( 37 |
  1. {user.username}
  2. 38 | ))} 39 |
40 | 41 |
42 | ) : ( 43 |

{error}

44 | )} 45 |
46 | ); 47 | }; 48 | 49 | export default Users; -------------------------------------------------------------------------------- /Chapter06/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /Chapter06/frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Chapter06/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /Chapter06/frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Chapter07/.env: -------------------------------------------------------------------------------- 1 | DB_URL=mongodb+srv:// 2 | DB_NAME=carBackend 3 | CLOUDINARY_SECRET_KEY= 4 | CLOUDINARY_API_KEY= 5 | CLOUDINARY_CLOUD_NAME= -------------------------------------------------------------------------------- /Chapter07/backend/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | # Environments 6 | .env 7 | .venv 8 | env/ 9 | venv/ 10 | ENV/ 11 | env.bak/ 12 | venv.bak/ 13 | secrets/ 14 | pics/ 15 | 16 | -------------------------------------------------------------------------------- /Chapter07/backend/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, status 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from motor import motor_asyncio 4 | from fastapi.exceptions import RequestValidationError 5 | from fastapi.responses import JSONResponse 6 | 7 | 8 | from fastapi.encoders import jsonable_encoder 9 | 10 | from collections import defaultdict 11 | 12 | from config import BaseConfig 13 | from routers.cars import router as cars_router 14 | from routers.users import router as users_router 15 | 16 | settings = BaseConfig() 17 | 18 | # define origins 19 | origins = ["*"] 20 | 21 | 22 | async def lifespan(app: FastAPI): 23 | app.client = motor_asyncio.AsyncIOMotorClient(settings.DB_URL) 24 | app.db = app.client[settings.DB_NAME] 25 | 26 | try: 27 | app.client.admin.command("ping") 28 | print("Pinged your deployment. You have successfully connected to MongoDB!") 29 | print("Mongo address:", settings.DB_URL) 30 | except Exception as e: 31 | print(e) 32 | yield 33 | app.client.close() 34 | 35 | 36 | app = FastAPI(lifespan=lifespan) 37 | 38 | 39 | # add CORS middleware 40 | app.add_middleware( 41 | CORSMiddleware, 42 | allow_origins=["*"], 43 | allow_credentials=True, 44 | allow_methods=["*"], 45 | allow_headers=["*"], 46 | ) 47 | 48 | 49 | @app.exception_handler(RequestValidationError) 50 | async def custom_form_validation_error(request, exc): 51 | reformatted_message = defaultdict(list) 52 | for pydantic_error in exc.errors(): 53 | loc, msg = pydantic_error["loc"], pydantic_error["msg"] 54 | filtered_loc = loc[1:] if loc[0] in ("body", "query", "path") else loc 55 | field_string = ".".join(filtered_loc) # nested fields with dot-notation 56 | reformatted_message[field_string].append(msg) 57 | 58 | return JSONResponse( 59 | status_code=status.HTTP_400_BAD_REQUEST, 60 | content=jsonable_encoder( 61 | {"detail": "Invalid request", "errors": reformatted_message} 62 | ), 63 | ) 64 | 65 | 66 | app.include_router(cars_router, prefix="/cars", tags=["cars"]) 67 | app.include_router(users_router, prefix="/users", tags=["users"]) 68 | 69 | 70 | @app.get("/") 71 | async def get_root(): 72 | return {"Message": "Root working"} 73 | -------------------------------------------------------------------------------- /Chapter07/backend/authentication.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import jwt 4 | from fastapi import HTTPException, Security 5 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer 6 | from passlib.context import CryptContext 7 | 8 | 9 | class AuthHandler: 10 | security = HTTPBearer() 11 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 12 | secret = "FARMSTACKsecretString" 13 | 14 | def get_password_hash(self, password): 15 | return self.pwd_context.hash(password) 16 | 17 | def verify_password(self, plain_password, hashed_password): 18 | return self.pwd_context.verify(plain_password, hashed_password) 19 | 20 | def encode_token(self, user_id, username): 21 | payload = { 22 | "exp": datetime.datetime.now(datetime.timezone.utc) 23 | + datetime.timedelta(minutes=30), 24 | "iat": datetime.datetime.now(datetime.timezone.utc), 25 | "sub": {"user_id": user_id, "username": username}, 26 | } 27 | return jwt.encode(payload, self.secret, algorithm="HS256") 28 | 29 | def decode_token(self, token): 30 | try: 31 | payload = jwt.decode(token, self.secret, algorithms=["HS256"]) 32 | return payload["sub"] 33 | except jwt.ExpiredSignatureError: 34 | raise HTTPException(status_code=401, detail="Signature has expired") 35 | except jwt.InvalidTokenError: 36 | raise HTTPException(status_code=401, detail="Invalid token") 37 | 38 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): 39 | return self.decode_token(auth.credentials) 40 | -------------------------------------------------------------------------------- /Chapter07/backend/config.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic_settings import BaseSettings, SettingsConfigDict 4 | 5 | 6 | class BaseConfig(BaseSettings): 7 | DB_URL: Optional[str] 8 | DB_NAME: Optional[str] 9 | CLOUDINARY_SECRET_KEY: Optional[str] 10 | CLOUDINARY_API_KEY: Optional[str] 11 | CLOUDINARY_CLOUD_NAME: Optional[str] 12 | 13 | """Loads the dotenv file. Including this is necessary to get 14 | pydantic to load a .env file.""" 15 | model_config = SettingsConfigDict(env_file=".env", extra="ignore") 16 | -------------------------------------------------------------------------------- /Chapter07/backend/models.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, List, Optional 2 | 3 | from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, field_validator 4 | 5 | # Represents an ObjectId field in the database. 6 | # It will be represented as a string in the model so that it can be serialized to JSON. 7 | 8 | PyObjectId = Annotated[str, BeforeValidator(str)] 9 | 10 | 11 | class CarModel(BaseModel): 12 | """ 13 | Container for a single car document in the database 14 | """ 15 | 16 | # The primary key for the CarModel, stored as a `str` on the instance. 17 | # This will be aliased to `_id` when sent to MongoDB, 18 | # but provided as `id` in the API requests and responses 19 | 20 | id: Optional[PyObjectId] = Field(alias="_id", default=None) 21 | brand: str = Field(...) 22 | make: str = Field(...) 23 | year: int = Field(..., gt=1970, lt=2025) 24 | cm3: int = Field(..., gt=0, lt=5000) 25 | km: int = Field(..., gt=0, lt=500 * 1000) 26 | price: int = Field(..., gt=0, lt=100000) 27 | user_id: str = Field(...) 28 | 29 | # add the picture file 30 | picture_url: Optional[str] = Field(None) 31 | 32 | @field_validator("brand") 33 | @classmethod 34 | def check_brand_case(cls, v: str) -> str: 35 | return v.title() 36 | 37 | @field_validator("make") 38 | @classmethod 39 | def check_make_case(cls, v: str) -> str: 40 | return v.title() 41 | 42 | model_config = ConfigDict( 43 | populate_by_name=True, 44 | arbitrary_types_allowed=True, 45 | json_schema_extra={ 46 | "example": { 47 | "brand": "Ford", 48 | "make": "Fiesta", 49 | "year": 2019, 50 | "cm3": 1500, 51 | "km": 120000, 52 | "price": 10000, 53 | "picture_url": "https://images.pexels.com/photos/2086676/pexels-photo-2086676.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", 54 | } 55 | }, 56 | ) 57 | 58 | 59 | class UpdateCarModel(BaseModel): 60 | """ 61 | Optional updates 62 | """ 63 | 64 | # The primary key for the CarModel, stored as a `str` on the instance. 65 | # This will be aliased to `_id` when sent to MongoDB, 66 | # but provided as `id` in the API requests and responses 67 | 68 | brand: Optional[str] = None 69 | make: Optional[str] = None 70 | year: Optional[int] = Field(gt=1970, lt=2025, default=None) 71 | cm3: Optional[int] = Field(gt=0, lt=5000, default=None) 72 | km: Optional[int] = Field(gt=0, lt=500 * 1000, default=None) 73 | price: Optional[int] = Field(gt=0, lt=100 * 1000, default=None) 74 | 75 | model_config = ConfigDict( 76 | populate_by_name=True, 77 | arbitrary_types_allowed=True, 78 | json_schema_extra={ 79 | "example": { 80 | "brand": "Ford", 81 | "make": "Fiesta", 82 | "year": 2019, 83 | "cm3": 1500, 84 | "km": 120000, 85 | "price": 10000, 86 | } 87 | }, 88 | ) 89 | 90 | 91 | class CarCollection(BaseModel): 92 | """ 93 | A container holding a list of cars 94 | """ 95 | 96 | cars: List[CarModel] 97 | 98 | 99 | class CarCollectionPagination(CarCollection): 100 | page: int = Field(ge=1, default=1) 101 | has_more: bool 102 | 103 | 104 | ######################### USER MODELS ################################### 105 | 106 | 107 | class UserModel(BaseModel): 108 | id: Optional[PyObjectId] = Field(alias="_id", default=None) 109 | username: str = Field(..., min_length=3, max_length=15) 110 | password: str = Field(...) 111 | 112 | 113 | class LoginModel(BaseModel): 114 | username: str = Field(...) 115 | password: str = Field(...) 116 | 117 | 118 | class CurrentUserModel(BaseModel): 119 | id: PyObjectId = Field(alias="_id", default=None) 120 | username: str = Field(..., min_length=3, max_length=15) 121 | -------------------------------------------------------------------------------- /Chapter07/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.111.0 2 | motor==3.4.0 3 | uvicorn==0.29.0 4 | httpie==3.2.2 5 | cloudinary==1.40.0 6 | pydantic-settings==2.2.1 7 | python-multipart==0.0.9 8 | passlib==1.7.4 9 | pyjwt==2.8.0 10 | bcrypt==4.0.1 -------------------------------------------------------------------------------- /Chapter07/backend/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter07/backend/routers/__init__.py -------------------------------------------------------------------------------- /Chapter07/backend/routers/cars.py: -------------------------------------------------------------------------------- 1 | import cloudinary 2 | from bson import ObjectId 3 | from cloudinary import uploader # noqa: F401 4 | from fastapi import ( 5 | APIRouter, 6 | Body, 7 | Depends, 8 | File, 9 | Form, 10 | HTTPException, 11 | Request, 12 | UploadFile, 13 | status, 14 | ) 15 | from fastapi.responses import Response 16 | from pymongo import ReturnDocument 17 | 18 | from authentication import AuthHandler 19 | from config import BaseConfig 20 | from models import CarCollectionPagination, CarModel, UpdateCarModel 21 | 22 | settings = BaseConfig() 23 | 24 | router = APIRouter() 25 | 26 | # instantiate the Auth Handler 27 | auth_handler = AuthHandler() 28 | 29 | CARS_PER_PAGE = 10 30 | 31 | 32 | cloudinary.config( 33 | cloud_name=settings.CLOUDINARY_CLOUD_NAME, 34 | api_key=settings.CLOUDINARY_API_KEY, 35 | api_secret=settings.CLOUDINARY_SECRET_KEY, 36 | ) 37 | 38 | # The post route handler without image uploads 39 | # @router.post( 40 | # "/", 41 | # response_description="Add new car", 42 | # response_model=CarModel, 43 | # status_code=status.HTTP_201_CREATED, 44 | # response_model_by_alias=False, 45 | # ) 46 | # async def add_car(request: Request, car: CarModel = Body(...)): 47 | # """Create a new car with a generated id.""" 48 | # cars = request.app.db["cars"] 49 | 50 | # document = car.model_dump(by_alias=True, exclude=["id"]) 51 | # inserted = await cars.insert_one(document) 52 | 53 | # return await cars.find_one({"_id": inserted.inserted_id}) 54 | 55 | 56 | @router.post( 57 | "/", 58 | response_description="Add new car with picture", 59 | response_model=CarModel, 60 | status_code=status.HTTP_201_CREATED, 61 | ) 62 | async def add_car_with_picture( 63 | request: Request, 64 | brand: str = Form("brand"), 65 | make: str = Form("make"), 66 | year: int = Form("year"), 67 | cm3: int = Form("cm3"), 68 | km: int = Form("km"), 69 | price: int = Form("price"), 70 | picture: UploadFile = File("picture"), 71 | user=Depends(auth_handler.auth_wrapper), 72 | ): 73 | """Upload picture to Cloudinary and create a new car with a generated id.""" 74 | 75 | cloudinary_image = cloudinary.uploader.upload( 76 | picture.file, folder="FARM2", crop="fill", width=800 77 | ) 78 | 79 | picture_url = cloudinary_image["url"] 80 | 81 | car = CarModel( 82 | brand=brand, 83 | make=make, 84 | year=year, 85 | cm3=cm3, 86 | km=km, 87 | price=price, 88 | picture_url=picture_url, 89 | user_id=user["user_id"], 90 | ) 91 | 92 | """Create a new car with a generated id.""" 93 | 94 | cars = request.app.db["cars"] 95 | 96 | document = car.model_dump(by_alias=True, exclude=["id"]) 97 | 98 | inserted = await cars.insert_one(document) 99 | 100 | return await cars.find_one({"_id": inserted.inserted_id}) 101 | 102 | 103 | # returns all cars without pagination 104 | 105 | # @router.get( 106 | # "/", 107 | # response_description="List all cars", 108 | # response_model=CarCollection, 109 | # response_model_by_alias=False, 110 | # ) 111 | # async def list_cars(request: Request): 112 | # """ 113 | # List all cars 114 | # """ 115 | # cars = request.app.db["cars"] 116 | 117 | # results = [] 118 | 119 | # cursor = cars.find() 120 | 121 | # async for document in cursor: 122 | # results.append(document) 123 | 124 | # return CarCollection(cars=results) 125 | 126 | 127 | @router.get( 128 | "/{id}", 129 | response_description="Get a single car by ID", 130 | response_model=CarModel, 131 | response_model_by_alias=False, 132 | ) 133 | async def show_car(id: str, request: Request): 134 | """ 135 | Get the record for a specific car, looked up by `id`. 136 | """ 137 | cars = request.app.db["cars"] 138 | 139 | # try to convert the ID to an ObjectId, otherwise 404: 140 | try: 141 | id = ObjectId(id) 142 | except Exception: 143 | raise HTTPException(status_code=404, detail=f"Car {id} not found") 144 | 145 | if (car := await cars.find_one({"_id": ObjectId(id)})) is not None: 146 | return car 147 | 148 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 149 | 150 | 151 | @router.get( 152 | "/", 153 | response_description="List all cars, paginated", 154 | response_model=CarCollectionPagination, 155 | response_model_by_alias=False, 156 | ) 157 | async def list_cars( 158 | request: Request, 159 | # user=Depends(auth_handler.auth_wrapper), 160 | page: int = 1, 161 | limit: int = CARS_PER_PAGE, 162 | ): 163 | cars = request.app.db["cars"] 164 | 165 | results = [] 166 | 167 | cursor = cars.find().sort("brand").limit(limit).skip((page - 1) * limit) 168 | 169 | total_documents = await cars.count_documents({}) 170 | has_more = total_documents > limit * page 171 | async for document in cursor: 172 | results.append(document) 173 | 174 | return CarCollectionPagination(cars=results, page=page, has_more=has_more) 175 | 176 | 177 | @router.put( 178 | "/{id}", 179 | response_description="Update car", 180 | response_model=CarModel, 181 | response_model_by_alias=False, 182 | ) 183 | async def update_car( 184 | id: str, 185 | request: Request, 186 | user=Depends(auth_handler.auth_wrapper), 187 | car: UpdateCarModel = Body(...), 188 | ): 189 | """ 190 | Update individual fields of an existing car record. 191 | 192 | Only the provided fields will be updated. 193 | Any missing or `null` fields will be ignored. 194 | """ 195 | 196 | # try to convert the ID to an ObjectId, otherwise 404: 197 | try: 198 | id = ObjectId(id) 199 | except Exception: 200 | raise HTTPException(status_code=404, detail=f"Car {id} not found") 201 | 202 | car = { 203 | k: v 204 | for k, v in car.model_dump(by_alias=True).items() 205 | if v is not None and k != "_id" 206 | } 207 | 208 | if len(car) >= 1: 209 | cars = request.app.db["cars"] 210 | 211 | update_result = await cars.find_one_and_update( 212 | {"_id": id}, 213 | {"$set": car}, 214 | return_document=ReturnDocument.AFTER, 215 | ) 216 | if update_result is not None: 217 | return update_result 218 | else: 219 | raise HTTPException(status_code=404, detail=f"Car {id} not found") 220 | 221 | # The update is empty, but we should still return the matching car: 222 | if (existing_car := await cars.find_one({"_id": id})) is not None: 223 | return existing_car 224 | 225 | raise HTTPException(status_code=404, detail=f"Car {id} not found") 226 | 227 | 228 | @router.delete("/{id}", response_description="Delete a car") 229 | async def delete_car( 230 | id: str, request: Request, user=Depends(auth_handler.auth_wrapper) 231 | ): 232 | """ 233 | Remove a single car. 234 | """ 235 | 236 | # try to convert the ID to an ObjectId, otherwise 404: 237 | try: 238 | id = ObjectId(id) 239 | except Exception: 240 | raise HTTPException(status_code=404, detail=f"Car {id} not found") 241 | 242 | cars = request.app.db["cars"] 243 | 244 | delete_result = await cars.delete_one({"_id": id}) 245 | 246 | if delete_result.deleted_count == 1: 247 | return Response(status_code=status.HTTP_204_NO_CONTENT) 248 | 249 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 250 | -------------------------------------------------------------------------------- /Chapter07/backend/routers/users.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response 3 | from fastapi.responses import JSONResponse 4 | 5 | from authentication import AuthHandler 6 | from models import CurrentUserModel, LoginModel, UserModel 7 | 8 | router = APIRouter() 9 | 10 | # instantiate the Auth Handler 11 | auth_handler = AuthHandler() 12 | 13 | # register user 14 | # validate the data and create a user if the username and the email are valid and available 15 | 16 | 17 | @router.post("/register", response_description="Register user") 18 | async def register(request: Request, newUser: LoginModel = Body(...)) -> UserModel: 19 | users = request.app.db["users"] 20 | 21 | # hash the password before inserting it into MongoDB 22 | newUser.password = auth_handler.get_password_hash(newUser.password) 23 | 24 | newUser = newUser.model_dump() 25 | 26 | # check existing user or email 409 Conflict: 27 | if ( 28 | existing_username := await users.find_one({"username": newUser["username"]}) 29 | is not None 30 | ): 31 | raise HTTPException( 32 | status_code=409, 33 | detail=f"User with username {newUser['username']} already exists", 34 | ) 35 | 36 | new_user = await users.insert_one(newUser) 37 | 38 | created_user = await users.find_one({"_id": new_user.inserted_id}) 39 | return created_user 40 | 41 | 42 | @router.post("/login", response_description="Login user") 43 | async def login(request: Request, loginUser: LoginModel = Body(...)) -> str: 44 | users = request.app.db["users"] 45 | 46 | # find the user by username 47 | user = await users.find_one({"username": loginUser.username}) 48 | 49 | # check password 50 | if (user is None) or ( 51 | not auth_handler.verify_password(loginUser.password, user["password"]) 52 | ): 53 | raise HTTPException(status_code=401, detail="Invalid username and/or password") 54 | 55 | token = auth_handler.encode_token(str(user["_id"]), user["username"]) 56 | 57 | response = JSONResponse(content={"token": token, "username": user["username"]}) 58 | 59 | return response 60 | 61 | 62 | # me route 63 | @router.get( 64 | "/me", response_description="Logged in user data", response_model=CurrentUserModel 65 | ) 66 | async def me( 67 | request: Request, response: Response, user_data=Depends(auth_handler.auth_wrapper) 68 | ): 69 | users = request.app.db["users"] 70 | currentUser = await users.find_one({"_id": ObjectId(user_data["user_id"])}) 71 | 72 | return currentUser 73 | -------------------------------------------------------------------------------- /Chapter07/backend/test_models.py: -------------------------------------------------------------------------------- 1 | from models import CarCollection, CarModel 2 | 3 | test_car_1 = CarModel( 4 | brand="ford", make="fiesta", year=2019, cm3=1500, km=120000, price=10000 5 | ) 6 | test_car_2 = CarModel( 7 | brand="fiat", make="stilo", year=2003, cm3=1600, km=320000, price=3000 8 | ) 9 | car_list = CarCollection(cars=[test_car_1, test_car_2]) 10 | print(car_list.model_dump()) 11 | -------------------------------------------------------------------------------- /Chapter08/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | "react/prop-types": 0, 17 | 'react-refresh/only-export-components': [ 18 | 'warn', 19 | { allowConstantExport: true }, 20 | ], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /Chapter08/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Chapter08/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /Chapter08/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Chapter08/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@hookform/resolvers": "^3.4.2", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-hook-form": "^7.51.5", 17 | "react-router-dom": "^6.23.1", 18 | "zod": "^3.23.8" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.2.66", 22 | "@types/react-dom": "^18.2.22", 23 | "@vitejs/plugin-react": "^4.2.1", 24 | "autoprefixer": "^10.4.19", 25 | "eslint": "^8.57.0", 26 | "eslint-plugin-react": "^7.34.1", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.6", 29 | "postcss": "^8.4.38", 30 | "tailwindcss": "^3.4.3", 31 | "vite": "^5.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Chapter08/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /Chapter08/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter08/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /Chapter08/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | createBrowserRouter, 3 | Route, 4 | createRoutesFromElements, 5 | RouterProvider 6 | } from "react-router-dom" 7 | 8 | import RootLayout from "./layouts/RootLayout" 9 | 10 | import Cars, { carsLoader } from "./pages/Cars" 11 | import Home from "./pages/Home" 12 | import Login from "./pages/Login" 13 | import NewCar from "./pages/NewCar" 14 | import SingleCar from "./pages/SingleCar" 15 | import NotFound from "./pages/NotFound" 16 | import AuthRequired from "./components/AuthRequired" 17 | 18 | import fetchCarData from "./utils/fetchCarData" 19 | 20 | import { AuthProvider } from "./contexts/AuthContext" 21 | 22 | const router = createBrowserRouter( 23 | createRoutesFromElements( 24 | }> 25 | } /> 26 | } loader={carsLoader} /> 27 | } /> 28 | }> 29 | } /> 30 | 31 | } 34 | loader={async ({ params }) => { 35 | return fetchCarData(params.id); 36 | }} 37 | errorElement={} /> 38 | } /> 39 | 40 | ) 41 | ) 42 | 43 | 44 | export default function App() { 45 | return ( 46 | 47 | 48 | 49 | ) 50 | } -------------------------------------------------------------------------------- /Chapter08/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter08/src/components/AuthRequired.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { Outlet, Navigate } from "react-router-dom" 3 | import { useAuth } from "../hooks/useAuth" 4 | 5 | const AuthRequired = () => { 6 | 7 | const { jwt } = useAuth() 8 | 9 | return ( 10 |
11 |

AuthRequired

12 | 13 | {jwt ? : } 14 | 15 |
16 | ) 17 | } 18 | export default AuthRequired -------------------------------------------------------------------------------- /Chapter08/src/components/CarCard.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | 3 | /* eslint react/prop-types: 0 */ 4 | const CarCard = ({ car }) => { 5 | 6 | return ( 7 |
8 | 9 |
{car.brand} {car.make} {car.year} {car.cm3} {car.price} {car.km}
10 | {car.make} 11 | 12 |
13 | ) 14 | } 15 | export default CarCard -------------------------------------------------------------------------------- /Chapter08/src/components/CarForm.jsx: -------------------------------------------------------------------------------- 1 | import { useForm } from "react-hook-form" 2 | import { z } from 'zod'; 3 | import { zodResolver } from '@hookform/resolvers/zod'; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | import { useAuth } from "../hooks/useAuth"; 7 | import InputField from "./InputField"; 8 | 9 | 10 | 11 | const schema = z.object({ 12 | brand: z.string().min(2, 'Brand must contain at least two letters').max(20, 'Brand cannot exceed 20 characters'), 13 | make: z.string().min(1, 'Car model must be at least 1 character long').max(20, 'Model cannot exceed 20 characters'), 14 | year: z.coerce.number().gte(1950).lte(2025), 15 | price: z.coerce.number().gte(100).lte(1000000), 16 | km: z.coerce.number().gte(0).lte(500000), 17 | cm3: z.coerce.number().gt(0).lte(5000), 18 | picture: z.any() 19 | .refine(file => file[0] && file[0].type.startsWith('image/'), { message: 'File must be an image' }) 20 | .refine(file => file[0] && file[0].size <= 1024 * 1024, { message: 'File size must be less than 1MB' }), 21 | 22 | }); 23 | 24 | 25 | 26 | // eslint-disable-next-line react/prop-types 27 | const CarForm = () => { 28 | 29 | const navigate = useNavigate(); 30 | const { jwt, setMessage } = useAuth(); 31 | const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({ 32 | resolver: zodResolver(schema), 33 | }); 34 | 35 | let formArray = [ 36 | 37 | { 38 | name: "brand", 39 | type: "text", 40 | error: errors.brand 41 | }, 42 | { 43 | name: "make", 44 | type: "text", 45 | error: errors.make 46 | }, 47 | { 48 | name: "year", 49 | type: "number", 50 | error: errors.year 51 | }, 52 | { 53 | name: "price", 54 | type: "number", 55 | error: errors.price 56 | }, 57 | { 58 | name: "km", 59 | type: "number", 60 | error: errors.km 61 | }, 62 | { 63 | name: "cm3", 64 | type: "number", 65 | error: errors.cm3 66 | }, 67 | { 68 | name: "picture", 69 | type: "file", 70 | error: errors.picture 71 | } 72 | 73 | ] 74 | 75 | const onSubmit = async (data) => { 76 | 77 | const formData = new FormData(); 78 | 79 | formArray.forEach((field) => { 80 | if (field == 'picture') { 81 | formData.append(field, data[field][0]); 82 | } else { 83 | 84 | formData.append(field.name, data[field.name]); 85 | } 86 | }); 87 | 88 | 89 | const result = await fetch(`${import.meta.env.VITE_API_URL}/cars/`, { 90 | method: 'POST', 91 | body: formData, 92 | headers: { 93 | Authorization: `Bearer ${jwt}`, 94 | } 95 | }); 96 | 97 | const json = await result.json(); 98 | 99 | if (result.ok) { 100 | navigate('/cars'); 101 | } else if (json.detail) { 102 | setMessage(JSON.stringify(json)) 103 | navigate('/') 104 | } 105 | 106 | 107 | } 108 | 109 | 110 | 111 | return ( 112 |
113 |
114 |
119 |

Insert new car

120 | 121 | {formArray.map((item, index) => ( 122 | 123 | 131 | ))} 132 | 133 | 134 | 135 |
136 | 142 |
143 | 144 |
145 |
146 | ) 147 | } 148 | export default CarForm -------------------------------------------------------------------------------- /Chapter08/src/components/InputField.jsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line react/prop-types 2 | const InputField = ({ props }) => { 3 | // eslint-disable-next-line react/prop-types 4 | const { name, type, error } = props 5 | return ( 6 | 7 |
8 | 11 | 21 | {error &&

{error.message}

} 22 |
23 | ) 24 | } 25 | export default InputField -------------------------------------------------------------------------------- /Chapter08/src/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { useForm } from "react-hook-form" 3 | import { z } from 'zod'; 4 | import { zodResolver } from '@hookform/resolvers/zod'; 5 | import { useAuth } from "../hooks/useAuth"; 6 | 7 | //import { useLogin } from "../hooks/useLogin" 8 | 9 | 10 | const schema = z.object({ 11 | username: z.string().min(4, 'Username must be at least 4 characters long').max(10, 'Username cannot exceed 10 characters'), 12 | password: z.string().min(4, 'Password must be at least 4 characters long').max(10, 'Password cannot exceed 10 characters'), 13 | }); 14 | 15 | const LoginForm = () => { 16 | 17 | const navigate = useNavigate(); 18 | const { login } = useAuth() 19 | 20 | const { register, handleSubmit, formState: { errors } } = useForm({ 21 | resolver: zodResolver(schema), 22 | }); 23 | 24 | const onSubmitForm = async (data) => { 25 | console.log(data) 26 | 27 | login(data.username, data.password) 28 | 29 | navigate('/') 30 | 31 | } 32 | 33 | 34 | 35 | return ( 36 |
37 |
38 |
42 | 43 |
44 | 47 | 55 | {errors.username &&

{errors.username.message}

} 56 |
57 |
58 | 61 | 69 | {errors.password &&

{errors.password.message}

} 70 |
71 |
72 | 79 |
80 |
81 |
82 |
83 | ) 84 | } 85 | export default LoginForm -------------------------------------------------------------------------------- /Chapter08/src/contexts/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useEffect } from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | 4 | export const AuthContext = createContext(); 5 | 6 | // eslint-disable-next-line react/prop-types 7 | export const AuthProvider = ({ children }) => { 8 | const [user, setUser] = useState(null); 9 | const [jwt, setJwt] = useState( 10 | localStorage.getItem('jwt') || null 11 | ); 12 | const [message, setMessage] = useState("Please log in"); 13 | 14 | useEffect(() => { 15 | 16 | const storedJwt = localStorage.getItem('jwt') || null; 17 | if (storedJwt) { 18 | setJwt(storedJwt); 19 | fetch(`${import.meta.env.VITE_API_URL}/users/me`, { 20 | headers: { 21 | Authorization: `Bearer ${storedJwt}`, 22 | }, 23 | }) 24 | .then(res => res.json()) 25 | 26 | .then(data => { 27 | 28 | if (data.username) { 29 | setUser({ user: data.username }); 30 | setMessage(`Welcome back, ${data.username}!`); 31 | } 32 | 33 | else { 34 | localStorage.removeItem('jwt'); 35 | setJwt(null); 36 | setUser(null); 37 | setMessage(data.message) 38 | 39 | } 40 | }) 41 | .catch(() => { 42 | localStorage.removeItem('jwt'); 43 | setJwt(null); 44 | setUser(null); 45 | setMessage('Please log in or register'); 46 | }); 47 | } else { 48 | setJwt(null); 49 | setUser(null); 50 | setMessage('Please log in or register'); 51 | } 52 | }, []); 53 | 54 | 55 | 56 | const login = async (username, password) => { 57 | 58 | 59 | const response = await fetch(`${import.meta.env.VITE_API_URL}/users/login`, 60 | { 61 | method: 'POST', 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | body: JSON.stringify({ username, password }), 66 | }); 67 | 68 | const data = await response.json(); 69 | if (response.ok) { 70 | 71 | setJwt(data.token); 72 | localStorage.setItem('jwt', data.token); 73 | setUser(data.username); 74 | setMessage(`Login successful: welcome ${data.username}`); 75 | 76 | } else { 77 | 78 | setMessage('Login failed: ' + data.detail); 79 | setUser(null); 80 | setJwt(null); 81 | localStorage.removeItem('jwt'); 82 | } 83 | 84 | return data 85 | }; 86 | 87 | const logout = () => { 88 | 89 | setUser(null); 90 | setJwt(null); 91 | localStorage.removeItem('jwt'); 92 | setMessage('Logout successful'); 93 | }; 94 | 95 | return ( 96 | 97 | {children} 98 | 99 | ); 100 | }; 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /Chapter08/src/hooks/useAuth.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { AuthContext } from "../contexts/AuthContext"; 3 | 4 | export const useAuth = () => { 5 | const context = useContext(AuthContext) 6 | 7 | if (!context) { 8 | throw new Error('Must be used within an AuthProvider') 9 | } 10 | 11 | return context 12 | } -------------------------------------------------------------------------------- /Chapter08/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /Chapter08/src/layouts/RootLayout.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { Outlet, NavLink } from "react-router-dom" 3 | import { useAuth } from "../hooks/useAuth" 4 | 5 | const RootLayout = () => { 6 | const { user, message, logout } = useAuth() 7 | 8 | 9 | return ( 10 |
11 |

RootLayout

12 | 13 | 14 |

{message}

15 | 16 | 17 | 18 |
19 | 30 |
31 |
32 | 33 |
34 |
35 | ) 36 | } 37 | export default RootLayout -------------------------------------------------------------------------------- /Chapter08/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Chapter08/src/pages/Cars.jsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData } from "react-router-dom" 2 | import CarCard from "../components/CarCard" 3 | const Cars = () => { 4 | const cars = useLoaderData() 5 | return ( 6 |
7 |

Available cars

8 | 9 |
10 | {cars.map(car => ( 11 | 12 | ))} 13 |
14 | 15 |
16 | ) 17 | } 18 | export default Cars 19 | 20 | export const carsLoader = async () => { 21 | 22 | const res = await fetch(`${import.meta.env.VITE_API_URL}/cars?limit=30`) 23 | 24 | const response = await res.json() 25 | 26 | if (!res.ok) { 27 | throw new Error(response.message) 28 | } 29 | 30 | return response['cars'] 31 | } -------------------------------------------------------------------------------- /Chapter08/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | const Home = () => { 2 | return ( 3 |
Home
4 | ) 5 | } 6 | export default Home -------------------------------------------------------------------------------- /Chapter08/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import LoginForm from "../components/LoginForm" 2 | const Login = () => { 3 | return ( 4 |
5 |

Login

6 | 7 |
8 | ) 9 | } 10 | export default Login -------------------------------------------------------------------------------- /Chapter08/src/pages/NewCar.jsx: -------------------------------------------------------------------------------- 1 | import CarForm from "../components/CarForm" 2 | 3 | const NewCar = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | export default NewCar -------------------------------------------------------------------------------- /Chapter08/src/pages/NotFound.jsx: -------------------------------------------------------------------------------- 1 | const NotFound = () => { 2 | return ( 3 |
This page does not exist yet!
4 | ) 5 | } 6 | export default NotFound -------------------------------------------------------------------------------- /Chapter08/src/pages/SingleCar.jsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData } from "react-router-dom"; 2 | import CarCard from "../components/CarCard"; 3 | 4 | 5 | const SingleCar = () => { 6 | 7 | const car = useLoaderData() 8 | 9 | return ( 10 | 11 | ); 12 | }; 13 | 14 | 15 | export default SingleCar 16 | 17 | -------------------------------------------------------------------------------- /Chapter08/src/utils/fetchCarData.js: -------------------------------------------------------------------------------- 1 | export default async function fetchCarData(id) { 2 | 3 | const res = await fetch(`${import.meta.env.VITE_API_URL}/cars/${id}`) 4 | 5 | const response = await res.json() 6 | 7 | if (!res.ok) { 8 | throw new Error(response.message) 9 | } 10 | 11 | return response 12 | } -------------------------------------------------------------------------------- /Chapter08/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /Chapter08/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Chapter09/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | # Environments 6 | .env 7 | .venv 8 | env/ 9 | venv/ 10 | ENV/ 11 | env.bak/ 12 | venv.bak/ 13 | secrets/ 14 | pics/ 15 | 16 | -------------------------------------------------------------------------------- /Chapter09/app.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from fastapi import FastAPI 4 | 5 | from database import init_db 6 | from routers import cars as cars_router 7 | from routers import user as user_router 8 | 9 | from fastapi_cors import CORS 10 | 11 | 12 | @asynccontextmanager 13 | async def lifespan(app: FastAPI): 14 | await init_db() 15 | yield 16 | 17 | 18 | app = FastAPI(lifespan=lifespan) 19 | 20 | CORS(app) 21 | 22 | 23 | # add CORS middleware 24 | # app.add_middleware( 25 | # CORSMiddleware, 26 | # allow_origins=["*"], 27 | # allow_credentials=True, 28 | # allow_methods=["*"], 29 | # allow_headers=["*"], 30 | # ) 31 | 32 | 33 | app.include_router(cars_router.router, prefix="/cars", tags=["Cars"]) 34 | app.include_router(user_router.router, prefix="/users", tags=["Users"]) 35 | 36 | 37 | @app.get("/", tags=["Root"]) 38 | async def read_root() -> dict: 39 | return {"message": "Welcome to your beanie powered app!"} 40 | -------------------------------------------------------------------------------- /Chapter09/authentication.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import jwt 4 | from fastapi import HTTPException, Security 5 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer 6 | from passlib.context import CryptContext 7 | 8 | 9 | class AuthHandler: 10 | security = HTTPBearer() 11 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 12 | secret = "FARMSTACKsecretString" 13 | 14 | def get_password_hash(self, password): 15 | return self.pwd_context.hash(password) 16 | 17 | def verify_password(self, plain_password, hashed_password): 18 | return self.pwd_context.verify(plain_password, hashed_password) 19 | 20 | def encode_token(self, user_id, username): 21 | payload = { 22 | "exp": datetime.datetime.now(datetime.timezone.utc) 23 | + datetime.timedelta(minutes=30), 24 | "iat": datetime.datetime.now(datetime.timezone.utc), 25 | "sub": {"user_id": user_id, "username": username}, 26 | } 27 | return jwt.encode(payload, self.secret, algorithm="HS256") 28 | 29 | def decode_token(self, token): 30 | try: 31 | payload = jwt.decode(token, self.secret, algorithms=["HS256"]) 32 | return payload["sub"] 33 | except jwt.ExpiredSignatureError: 34 | raise HTTPException(status_code=401, detail="Signature has expired") 35 | except jwt.InvalidTokenError: 36 | raise HTTPException(status_code=401, detail="Invalid token") 37 | 38 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): 39 | return self.decode_token(auth.credentials) 40 | -------------------------------------------------------------------------------- /Chapter09/background.py: -------------------------------------------------------------------------------- 1 | import json 2 | from time import sleep 3 | 4 | import resend 5 | from openai import OpenAI 6 | 7 | from config import BaseConfig 8 | from models import Car 9 | 10 | settings = BaseConfig() 11 | 12 | client = OpenAI(api_key=settings.OPENAI_API_KEY) 13 | 14 | resend.api_key = settings.RESEND_API_KEY 15 | 16 | 17 | def generate_prompt(brand: str, model: str, year: int) -> str: 18 | return f""" 19 | You are a helpful car sales assistant. Describe the {brand} {model} from {year} in a playful manner. 20 | Also, provide five pros and five cons of the model, but formulate the cons in a not overly negative way. 21 | You will respond with a JSON format consisting of the following: 22 | a brief description of the {brand} {model}, playful and positive, but not over the top. 23 | This will be called *description*. Make it at least 350 characters. 24 | an array of 5 brief *pros* of the car model, short and concise, maximum 12 words, slightly positive and playful 25 | an array of 5 brief *cons* drawbacks of the car model, short and concise, maximum 12 words, not too negative, but in a slightly negative tone 26 | make the *pros* sound very positive and the *cons* sound negative, but not too much 27 | """ 28 | 29 | 30 | def delayed_task(username: str) -> None: 31 | sleep(5) 32 | print(f"User just logged in: {username}") 33 | 34 | 35 | async def create_description(brand, make, year, picture_url): 36 | prompt = generate_prompt(brand, make, year) 37 | 38 | try: 39 | response = client.chat.completions.create( 40 | model="gpt-4", 41 | messages=[{"role": "user", "content": prompt}], 42 | max_tokens=500, 43 | temperature=0.2, 44 | ) 45 | content = response.choices[0].message.content 46 | json_part = content 47 | 48 | # Parsing the JSON string into a Python dictionary 49 | car_info = json.loads(json_part) 50 | 51 | await Car.find(Car.brand == brand, Car.make == make, Car.year == year).set( 52 | { 53 | "description": car_info["description"], 54 | "pros": car_info["pros"], 55 | "cons": car_info["cons"], 56 | } 57 | ) 58 | 59 | def generate_email(): 60 | pros_list = "
".join([f"- {pro}" for pro in car_info["pros"]]) 61 | cons_list = "
".join([f"- {con}" for con in car_info["cons"]]) 62 | 63 | return f""" 64 | Hello, 65 | We have a new car for you: {brand} {make} from {year}. 66 |

67 | {car_info['description']} 68 |

Pros

69 | {pros_list} 70 |

Cons

71 | {cons_list} 72 | """ 73 | 74 | params: resend.Emails.SendParams = { 75 | "from": "FARM Cars ", 76 | "to": ["aleksendric@gmail.com"], 77 | "subject": "New car on sale!", 78 | "html": generate_email(), 79 | } 80 | 81 | resend.Emails.send(params) 82 | 83 | return True 84 | 85 | except Exception as e: 86 | print(e) 87 | -------------------------------------------------------------------------------- /Chapter09/config.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic_settings import BaseSettings, SettingsConfigDict 4 | 5 | 6 | class BaseConfig(BaseSettings): 7 | DB_URL: Optional[str] 8 | 9 | CLOUDINARY_SECRET_KEY: Optional[str] 10 | CLOUDINARY_API_KEY: Optional[str] 11 | CLOUDINARY_CLOUD_NAME: Optional[str] 12 | 13 | OPENAI_API_KEY: Optional[str] 14 | RESEND_API_KEY: Optional[str] 15 | 16 | """Loads the dotenv file. Including this is necessary to get 17 | pydantic to load a .env file.""" 18 | model_config = SettingsConfigDict(env_file=".env", extra="ignore") 19 | -------------------------------------------------------------------------------- /Chapter09/database.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from beanie import init_beanie 3 | 4 | from config import BaseConfig 5 | from models import Car, User 6 | 7 | settings = BaseConfig() 8 | 9 | 10 | async def init_db(): 11 | client = motor.motor_asyncio.AsyncIOMotorClient(settings.DB_URL) 12 | await init_beanie(database=client.carAds, document_models=[User, Car]) 13 | -------------------------------------------------------------------------------- /Chapter09/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional 3 | 4 | from beanie import Document, Link, PydanticObjectId 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class User(Document): 9 | username: str = Field(min_length=3, max_length=50) 10 | password: str 11 | email: str 12 | 13 | created: datetime = Field(default_factory=datetime.now) 14 | 15 | class Settings: 16 | name = "user" 17 | 18 | class Config: 19 | json_schema_extra = { 20 | "example": { 21 | "username": "John", 22 | "password": "password", 23 | "email": "john@mail.com", 24 | } 25 | } 26 | 27 | 28 | class RegisterUser(BaseModel): 29 | username: str 30 | password: str 31 | email: str 32 | 33 | 34 | class LoginUser(BaseModel): 35 | username: str 36 | password: str 37 | 38 | 39 | class CurrentUser(BaseModel): 40 | username: str 41 | email: str 42 | id: PydanticObjectId 43 | 44 | 45 | class Car(Document, extra="allow"): 46 | brand: str 47 | make: str 48 | year: int 49 | cm3: int 50 | price: float 51 | description: Optional[str] = None 52 | picture_url: Optional[str] = None 53 | pros: List[str] = [] 54 | cons: List[str] = [] 55 | date: datetime = datetime.now() 56 | user: Link[User] = None 57 | 58 | class Settings: 59 | name = "car" 60 | 61 | class Config: 62 | json_schema_extra = { 63 | "example": { 64 | "brand": "BMW", 65 | "make": "X5", 66 | "year": 2021, 67 | "cm3": 3000, 68 | "price": 100000, 69 | } 70 | } 71 | 72 | 73 | class UpdateCar(BaseModel): 74 | price: Optional[float] = None 75 | description: Optional[str] = None 76 | pros: Optional[List[str]] = None 77 | cons: Optional[List[str]] = None 78 | -------------------------------------------------------------------------------- /Chapter09/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter09/requirements.txt -------------------------------------------------------------------------------- /Chapter09/routers/cars.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import cloudinary 4 | 5 | from beanie import PydanticObjectId, WriteRules 6 | 7 | from cloudinary import uploader # noqa: F401 8 | from fastapi import ( 9 | APIRouter, 10 | BackgroundTasks, 11 | Depends, 12 | File, 13 | Form, 14 | HTTPException, 15 | UploadFile, 16 | status, 17 | ) 18 | 19 | from authentication import AuthHandler 20 | from background import create_description 21 | from config import BaseConfig 22 | from models import Car, UpdateCar, User 23 | 24 | auth_handler = AuthHandler() 25 | 26 | 27 | settings = BaseConfig() 28 | 29 | cloudinary.config( 30 | cloud_name=settings.CLOUDINARY_CLOUD_NAME, 31 | api_key=settings.CLOUDINARY_API_KEY, 32 | api_secret=settings.CLOUDINARY_SECRET_KEY, 33 | ) 34 | 35 | 36 | router = APIRouter() 37 | 38 | 39 | @router.get("/", response_model=List[Car]) 40 | async def get_cars(): 41 | """Get all cars from the database.""" 42 | 43 | return await Car.find_all().to_list() 44 | 45 | 46 | @router.get("/{car_id}", response_model=Car) 47 | async def get_car(car_id: PydanticObjectId): 48 | car = await Car.get(car_id) 49 | if not car: 50 | raise HTTPException(status_code=404, detail="Car not found") 51 | return car 52 | 53 | 54 | # @router.post("/", response_model=Car) 55 | # async def create_car(car: Car, background_tasks: BackgroundTasks): 56 | # user = await User.find().first_or_none() 57 | # print("User:", user) 58 | # car.user = user 59 | # print("Car:", car) 60 | # background_tasks.add_task(delayed_task, message="Inserting car...") 61 | # return await car.insert(link_rule=WriteRules.WRITE) 62 | 63 | 64 | @router.post( 65 | "/", 66 | response_description="Add new car with picture", 67 | response_model=Car, 68 | status_code=status.HTTP_201_CREATED, 69 | ) 70 | async def add_car_with_picture( 71 | background_tasks: BackgroundTasks, 72 | brand: str = Form("brand"), 73 | make: str = Form("make"), 74 | year: int = Form("year"), 75 | cm3: int = Form("cm3"), 76 | km: int = Form("km"), 77 | price: int = Form("price"), 78 | picture: UploadFile = File("picture"), 79 | user_data=Depends(auth_handler.auth_wrapper), 80 | ): 81 | """Upload picture to Cloudinary and create a new car with a generated id.""" 82 | 83 | cloudinary_image = cloudinary.uploader.upload( 84 | picture.file, folder="FARM2", crop="fill", width=800, height=600, gravity="auto" 85 | ) 86 | 87 | picture_url = cloudinary_image["url"] 88 | user = await User.get(user_data["user_id"]) 89 | 90 | car = Car( 91 | brand=brand, 92 | make=make, 93 | year=year, 94 | cm3=cm3, 95 | km=km, 96 | price=price, 97 | picture_url=picture_url, 98 | user=user, 99 | ) 100 | 101 | """Create a new car with a generated id.""" 102 | 103 | background_tasks.add_task( 104 | create_description, brand=brand, make=make, year=year, picture_url=picture_url 105 | ) 106 | 107 | return await car.insert(link_rule=WriteRules.WRITE) 108 | 109 | 110 | @router.put("/{car_id}", response_model=Car) 111 | async def update_car(car_id: PydanticObjectId, cardata: UpdateCar): 112 | car = await Car.get(car_id) 113 | if not car: 114 | raise HTTPException(status_code=404, detail="Car not found") 115 | updated_car = {k: v for k, v in cardata.model_dump().items() if v is not None} 116 | 117 | return await car.set(updated_car) 118 | 119 | 120 | @router.delete("/{car_id}") 121 | async def delete_car(car_id: PydanticObjectId): 122 | car = await Car.get(car_id) 123 | if not car: 124 | raise HTTPException(status_code=404, detail="Car not found") 125 | await car.delete() 126 | -------------------------------------------------------------------------------- /Chapter09/routers/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException 2 | from fastapi.responses import JSONResponse 3 | 4 | from authentication import AuthHandler 5 | from background import delayed_task 6 | from models import CurrentUser, LoginUser, RegisterUser, User 7 | 8 | auth_handler = AuthHandler() 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.post( 14 | "/register", response_description="Register user", response_model=CurrentUser 15 | ) 16 | async def register(newUser: RegisterUser = Body(...), response_model=User): 17 | newUser.password = auth_handler.get_password_hash(newUser.password) 18 | query = {"$or": [{"username": newUser.username}, {"email": newUser.email}]} 19 | existing_user = await User.find_one(query) 20 | 21 | # check existing user or email 409 Conflict: 22 | if existing_user is not None: 23 | raise HTTPException( 24 | status_code=409, 25 | detail=f"User with username {newUser.username} or email {newUser.email} already exists", 26 | ) 27 | 28 | user = await User(**newUser.model_dump()).save() 29 | 30 | return user 31 | 32 | 33 | @router.post("/login", response_description="Login user and return token") 34 | async def login( 35 | background_tasks: BackgroundTasks, loginUser: LoginUser = Body(...) 36 | ) -> str: 37 | # find the user by username 38 | user = await User.find_one(User.username == loginUser.username) 39 | 40 | if user and auth_handler.verify_password(loginUser.password, user.password): 41 | token = auth_handler.encode_token(str(user.id), user.username) 42 | background_tasks.add_task(delayed_task, username=user.username) 43 | 44 | response = JSONResponse(content={"token": token, "username": user.username}) 45 | return response 46 | else: 47 | raise HTTPException(status_code=401, detail="Invalid username or password") 48 | 49 | 50 | @router.get( 51 | "/me", response_description="Logged in user data", response_model=CurrentUser 52 | ) 53 | async def me(user_data=Depends(auth_handler.auth_wrapper)): 54 | currentUser = await User.get(user_data["user_id"]) 55 | 56 | return currentUser 57 | -------------------------------------------------------------------------------- /Chapter10/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /Chapter10/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /Chapter10/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /Chapter10/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Chapter10/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | hostname: 'res.cloudinary.com', 7 | }, 8 | ] 9 | } 10 | }; 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /Chapter10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ch10next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "iron-session": "^8.0.2", 13 | "next": "14.2.4", 14 | "react": "^18", 15 | "react-dom": "^18" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^8", 19 | "eslint-config-next": "14.2.4", 20 | "postcss": "^8", 21 | "tailwindcss": "^3.4.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter10/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /Chapter10/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter10/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter10/sample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "brand": "Ford", 4 | "make": "Focus", 5 | "year": 2015, 6 | "cm3": 1596, 7 | "price": 9500, 8 | "km": 75000, 9 | "description": "The Ford Focus is your trusty sidekick for urban adventures, blending style with practicality in a compact package.", 10 | "pros": [ 11 | "Great fuel efficiency", 12 | "Comfortable ride", 13 | "Affordable maintenance", 14 | "Good handling", 15 | "Spacious interior" 16 | ], 17 | "cons": [ 18 | "Limited rear visibility", 19 | "Average acceleration", 20 | "Basic interior materials", 21 | "Limited tech features", 22 | "Road noise" 23 | ] 24 | }, 25 | { 26 | "brand": "Fiat", 27 | "make": "500", 28 | "year": 2018, 29 | "cm3": 1242, 30 | "price": 8000, 31 | "km": 45000, 32 | "description": "The Fiat 500 is a charming little car that adds a touch of Italian flair to your daily drive, perfect for city living.", 33 | "pros": [ 34 | "Compact size", 35 | "Stylish design", 36 | "Easy to park", 37 | "Good fuel economy", 38 | "Affordable" 39 | ], 40 | "cons": [ 41 | "Limited cargo space", 42 | "Small rear seats", 43 | "Modest performance", 44 | "Average build quality", 45 | "Highway noise" 46 | ] 47 | }, 48 | { 49 | "brand": "Peugeot", 50 | "make": "208", 51 | "year": 2017, 52 | "cm3": 1560, 53 | "price": 9500, 54 | "km": 60000, 55 | "description": "The Peugeot 208 is a small car with a big personality, offering a delightful driving experience with a French twist.", 56 | "pros": [ 57 | "Modern design", 58 | "Efficient engines", 59 | "Comfortable interior", 60 | "Advanced tech", 61 | "Good safety features" 62 | ], 63 | "cons": [ 64 | "Limited rear space", 65 | "Firm ride", 66 | "Higher maintenance costs", 67 | "Complex infotainment", 68 | "Average resale value" 69 | ] 70 | }, 71 | { 72 | "brand": "Audi", 73 | "make": "A3", 74 | "year": 2016, 75 | "cm3": 1395, 76 | "price": 15000, 77 | "km": 70000, 78 | "description": "The Audi A3 is the epitome of elegance and performance, turning heads while providing a luxurious ride.", 79 | "pros": [ 80 | "Premium interior", 81 | "Strong performance", 82 | "Advanced technology", 83 | "Smooth ride", 84 | "High safety rating" 85 | ], 86 | "cons": [ 87 | "Expensive maintenance", 88 | "Limited rear space", 89 | "High insurance costs", 90 | "Firm suspension", 91 | "Complex infotainment system" 92 | ] 93 | }, 94 | { 95 | "brand": "Citroen", 96 | "make": "C3", 97 | "year": 2019, 98 | "cm3": 1199, 99 | "price": 12000, 100 | "km": 30000, 101 | "description": "The Citroen C3 is a quirky and comfortable car that brings a splash of color and fun to your daily commute.", 102 | "pros": [ 103 | "Unique design", 104 | "Comfortable ride", 105 | "Good fuel efficiency", 106 | "Affordable price", 107 | "Spacious interior" 108 | ], 109 | "cons": [ 110 | "Mediocre performance", 111 | "Basic interior materials", 112 | "Limited tech features", 113 | "Average handling", 114 | "Small boot space" 115 | ] 116 | }, 117 | { 118 | "brand": "Volkswagen", 119 | "make": "Golf", 120 | "year": 2014, 121 | "cm3": 1395, 122 | "price": 10000, 123 | "km": 90000, 124 | "description": "The Volkswagen Golf is a classic hatchback that combines practicality with a dash of sporty flair.", 125 | "pros": [ 126 | "Solid build quality", 127 | "Efficient engines", 128 | "Comfortable ride", 129 | "Spacious interior", 130 | "Good resale value" 131 | ], 132 | "cons": [ 133 | "Expensive to maintain", 134 | "Dated infotainment system", 135 | "Average performance", 136 | "Firm suspension", 137 | "Limited standard features" 138 | ] 139 | }, 140 | { 141 | "brand": "Ford", 142 | "make": "Fiesta", 143 | "year": 2016, 144 | "cm3": 998, 145 | "price": 8500, 146 | "km": 65000, 147 | "description": "The Ford Fiesta is a nimble and fun-to-drive car that's perfect for zipping through city streets.", 148 | "pros": [ 149 | "Excellent handling", 150 | "Fuel efficient", 151 | "Compact size", 152 | "Affordable maintenance", 153 | "Modern design" 154 | ], 155 | "cons": [ 156 | "Limited rear space", 157 | "Basic interior materials", 158 | "Average acceleration", 159 | "Road noise", 160 | "Small boot space" 161 | ] 162 | }, 163 | { 164 | "brand": "Fiat", 165 | "make": "Punto", 166 | "year": 2017, 167 | "cm3": 1242, 168 | "price": 7000, 169 | "km": 80000, 170 | "description": "The Fiat Punto is a practical and stylish car that offers a great balance between comfort and efficiency.", 171 | "pros": [ 172 | "Good fuel economy", 173 | "Spacious interior", 174 | "Affordable price", 175 | "Easy to drive", 176 | "Compact size" 177 | ], 178 | "cons": [ 179 | "Average performance", 180 | "Basic interior", 181 | "Limited tech features", 182 | "Firm ride", 183 | "Average build quality" 184 | ] 185 | }, 186 | { 187 | "brand": "Peugeot", 188 | "make": "308", 189 | "year": 2018, 190 | "cm3": 1560, 191 | "price": 11000, 192 | "km": 50000, 193 | "description": "The Peugeot 308 is a refined and stylish hatchback that brings a touch of sophistication to everyday driving.", 194 | "pros": [ 195 | "Modern design", 196 | "Comfortable ride", 197 | "Efficient engines", 198 | "Advanced safety features", 199 | "Spacious interior" 200 | ], 201 | "cons": [ 202 | "Higher maintenance costs", 203 | "Limited rear visibility", 204 | "Firm suspension", 205 | "Complex infotainment", 206 | "Average resale value" 207 | ] 208 | }, 209 | { 210 | "brand": "Audi", 211 | "make": "A4", 212 | "year": 2015, 213 | "cm3": 1984, 214 | "price": 17000, 215 | "km": 80000, 216 | "description": "The Audi A4 is a premium sedan that exudes class and performance, making every journey a pleasure.", 217 | "pros": [ 218 | "Luxurious interior", 219 | "Strong performance", 220 | "Advanced technology", 221 | "Smooth ride", 222 | "High safety rating" 223 | ], 224 | "cons": [ 225 | "Expensive maintenance", 226 | "High insurance costs", 227 | "Limited rear space", 228 | "Firm suspension", 229 | "Complex infotainment system" 230 | ] 231 | }, 232 | { 233 | "brand": "Citroen", 234 | "make": "C4", 235 | "year": 2016, 236 | "cm3": 1560, 237 | "price": 9500, 238 | "km": 70000, 239 | "description": "The Citroen C4 is a comfortable and practical car that adds a touch of French flair to your daily drive.", 240 | "pros": [ 241 | "Comfortable ride", 242 | "Good fuel efficiency", 243 | "Spacious interior", 244 | "Unique design", 245 | "Affordable price" 246 | ], 247 | "cons": [ 248 | "Average performance", 249 | "Basic interior materials", 250 | "Limited tech features", 251 | "Firm suspension", 252 | "Small boot space" 253 | ] 254 | }, 255 | { 256 | "brand": "Volkswagen", 257 | "make": "Passat", 258 | "year": 2017, 259 | "cm3": 1968, 260 | "price": 18000, 261 | "km": 60000, 262 | "description": "The Volkswagen Passat is a sophisticated and reliable sedan that offers a perfect blend of comfort and performance.", 263 | "pros": [ 264 | "Spacious interior", 265 | "Solid build quality", 266 | "Efficient engines", 267 | "Comfortable ride", 268 | "Advanced tech features" 269 | ], 270 | "cons": [ 271 | "Expensive to maintain", 272 | "Average performance", 273 | "Firm suspension", 274 | "Complex infotainment", 275 | "High insurance costs" 276 | ] 277 | }, 278 | { 279 | "brand": "Ford", 280 | "make": "Mondeo", 281 | "year": 2016, 282 | "cm3": 1999, 283 | "price": 12000, 284 | "km": 85000, 285 | "description": "The Ford Mondeo is a reliable and spacious sedan that makes every journey comfortable and enjoyable.", 286 | "pros": [ 287 | "Spacious interior", 288 | "Comfortable ride", 289 | "Good fuel efficiency", 290 | "Solid build quality", 291 | "Advanced safety features" 292 | ], 293 | "cons": [ 294 | "Limited rear visibility", 295 | "Average performance", 296 | "Basic interior materials", 297 | "Firm suspension", 298 | "Complex infotainment" 299 | ] 300 | }, 301 | { 302 | "brand": "Fiat", 303 | "make": "Tipo", 304 | "year": 2019, 305 | "cm3": 1368, 306 | "price": 13000, 307 | "km": 30000, 308 | "description": "The Fiat Tipo is a practical and stylish car that offers great value for money, perfect for families.", 309 | "pros": [ 310 | "Spacious 311 | 312 | interior","Good fuel efficiency", 313 | "Affordable price", 314 | "Comfortable ride", 315 | "Modern design" 316 | ], 317 | "cons": [ 318 | "Average performance", 319 | "Basic interior materials", 320 | "Limited tech features", 321 | "Firm suspension", 322 | "Average build quality" 323 | ] 324 | }, 325 | { 326 | "brand": "Peugeot", 327 | "make": "3008", 328 | "year": 2018, 329 | "cm3": 1598, 330 | "price": 20000, 331 | "km": 40000, 332 | "description": "The Peugeot 3008 is a stylish and versatile SUV that offers a perfect blend of comfort, space, and technology.", 333 | "pros": [ 334 | "Modern design", 335 | "Spacious interior", 336 | "Advanced tech features", 337 | "Comfortable ride", 338 | "Good fuel efficiency" 339 | ], 340 | "cons": [ 341 | "Higher maintenance costs", 342 | "Limited rear visibility", 343 | "Firm suspension", 344 | "Complex infotainment", 345 | "Average resale value" 346 | ] 347 | }, 348 | { 349 | "brand": "Audi", 350 | "make": "Q3", 351 | "year": 2017, 352 | "cm3": 1395, 353 | "price": 25000, 354 | "km": 50000, 355 | "description": "The Audi Q3 is a premium compact SUV that offers luxury, performance, and versatility in a stylish package.", 356 | "pros": [ 357 | "Luxurious interior", 358 | "Strong performance", 359 | "Advanced technology", 360 | "Smooth ride", 361 | "High safety rating" 362 | ], 363 | "cons": [ 364 | "Expensive maintenance", 365 | "High insurance costs", 366 | "Limited rear space", 367 | "Firm suspension", 368 | "Complex infotainment system" 369 | ] 370 | }, 371 | { 372 | "brand": "Citroen", 373 | "make": "C5 Aircross", 374 | "year": 2019, 375 | "cm3": 1499, 376 | "price": 22000, 377 | "km": 25000, 378 | "description": "The Citroen C5 Aircross is a comfortable and spacious SUV that brings a touch of French elegance to your adventures.", 379 | "pros": [ 380 | "Comfortable ride", 381 | "Spacious interior", 382 | "Unique design", 383 | "Advanced tech features", 384 | "Good fuel efficiency" 385 | ], 386 | "cons": [ 387 | "Average performance", 388 | "Basic interior materials", 389 | "Limited rear visibility", 390 | "Firm suspension", 391 | "Complex infotainment" 392 | ] 393 | }, 394 | { 395 | "brand": "Volkswagen", 396 | "make": "Tiguan", 397 | "year": 2016, 398 | "cm3": 1968, 399 | "price": 23000, 400 | "km": 40000, 401 | "description": "The Volkswagen Tiguan is a stylish and versatile SUV that offers a great blend of comfort, space, and technology.", 402 | "pros": [ 403 | "Solid build quality", 404 | "Comfortable ride", 405 | "Spacious interior", 406 | "Efficient engines", 407 | "Advanced tech features" 408 | ], 409 | "cons": [ 410 | "Expensive to maintain", 411 | "Average performance", 412 | "Firm suspension", 413 | "Complex infotainment", 414 | "High insurance costs" 415 | ] 416 | }, 417 | { 418 | "brand": "Ford", 419 | "make": "Kuga", 420 | "year": 2017, 421 | "cm3": 1498, 422 | "price": 21000, 423 | "km": 35000, 424 | "description": "The Ford Kuga is a practical and stylish SUV that offers great value for money, perfect for families.", 425 | "pros": [ 426 | "Spacious interior", 427 | "Comfortable ride", 428 | "Good fuel efficiency", 429 | "Modern design", 430 | "Advanced safety features" 431 | ], 432 | "cons": [ 433 | "Average performance", 434 | "Basic interior materials", 435 | "Limited tech features", 436 | "Firm suspension", 437 | "Average build quality" 438 | ] 439 | }, 440 | { 441 | "brand": "Fiat", 442 | "make": "500X", 443 | "year": 2018, 444 | "cm3": 1368, 445 | "price": 18000, 446 | "km": 30000, 447 | "description": "The Fiat 500X is a charming and versatile compact SUV that adds a touch of Italian style to your daily drive.", 448 | "pros": [ 449 | "Compact size", 450 | "Stylish design", 451 | "Good fuel economy", 452 | "Comfortable ride", 453 | "Affordable" 454 | ], 455 | "cons": [ 456 | "Limited cargo space", 457 | "Small rear seats", 458 | "Modest performance", 459 | "Average build quality", 460 | "Highway noise" 461 | ] 462 | }, 463 | { 464 | "brand": "Peugeot", 465 | "make": "5008", 466 | "year": 2019, 467 | "cm3": 1598, 468 | "price": 25000, 469 | "km": 20000, 470 | "description": "The Peugeot 5008 is a stylish and spacious SUV that offers a perfect blend of comfort, space, and technology.", 471 | "pros": [ 472 | "Modern design", 473 | "Spacious interior", 474 | "Advanced tech features", 475 | "Comfortable ride", 476 | "Good fuel efficiency" 477 | ], 478 | "cons": [ 479 | "Higher maintenance costs", 480 | "Limited rear visibility", 481 | "Firm suspension", 482 | "Complex infotainment", 483 | "Average resale value" 484 | ] 485 | }, 486 | { 487 | "brand": "Audi", 488 | "make": "Q5", 489 | "year": 2017, 490 | "cm3": 1968, 491 | "price": 30000, 492 | "km": 50000, 493 | "description": "The Audi Q5 is a premium mid-size SUV that offers luxury, performance, and versatility in a stylish package.", 494 | "pros": [ 495 | "Luxurious interior", 496 | "Strong performance", 497 | "Advanced technology", 498 | "Smooth ride", 499 | "High safety rating" 500 | ], 501 | "cons": [ 502 | "Expensive maintenance", 503 | "High insurance costs", 504 | "Limited rear space", 505 | "Firm suspension", 506 | "Complex infotainment system" 507 | ] 508 | }, 509 | { 510 | "brand": "Citroen", 511 | "make": "Berlingo", 512 | "year": 2016, 513 | "cm3": 1560, 514 | "price": 14000, 515 | "km": 60000, 516 | "description": "The Citroen Berlingo is a practical and spacious MPV that offers great versatility for families and businesses.", 517 | "pros": [ 518 | "Spacious interior", 519 | "Good fuel efficiency", 520 | "Practical design", 521 | "Affordable price", 522 | "Comfortable ride" 523 | ], 524 | "cons": [ 525 | "Average performance", 526 | "Basic interior materials", 527 | "Limited tech features", 528 | "Firm suspension", 529 | "Average build quality" 530 | ] 531 | }, 532 | { 533 | "brand": "Volkswagen", 534 | "make": "Touran", 535 | "year": 2017, 536 | "cm3": 1598, 537 | "price": 19000, 538 | "km": 40000, 539 | "description": "The Volkswagen Touran is a practical and spacious MPV that offers great versatility for families and businesses.", 540 | "pros": [ 541 | "Solid build quality", 542 | "Comfortable ride", 543 | "Spacious interior", 544 | "Efficient engines", 545 | "Advanced tech features" 546 | ], 547 | "cons": [ 548 | "Expensive to maintain", 549 | "Average performance", 550 | "Firm suspension", 551 | "Complex infotainment", 552 | "High insurance costs" 553 | ] 554 | }, 555 | { 556 | "brand": "Ford", 557 | "make": "Galaxy", 558 | "year": 2016, 559 | "cm3": 1999, 560 | "price": 20000, 561 | "km": 75000, 562 | "description": "The Ford Galaxy is a spacious and practical MPV that offers great versatility for families and businesses.", 563 | "pros": [ 564 | "Spacious interior", 565 | "Comfortable ride", 566 | "Good fuel efficiency", 567 | "Solid build quality", 568 | "Advanced safety features" 569 | ], 570 | "cons": [ 571 | "Limited rear visibility", 572 | "Average performance", 573 | "Basic interior materials", 574 | "Firm suspension", 575 | "Complex infotainment" 576 | ] 577 | }, 578 | { 579 | "brand": "Fiat", 580 | "make": "Ducato", 581 | "year": 2019, 582 | "cm3": 2287, 583 | "price": 22000, 584 | "km": 50000, 585 | "description": "The Fiat Ducato is a versatile and reliable van that offers great space and practicality for businesses.", 586 | "pros": [ 587 | "Spacious interior", 588 | "Good fuel efficiency", 589 | "Practical design", 590 | "Affordable price", 591 | "Comfortable ride" 592 | ], 593 | "cons": [ 594 | "Average performance", 595 | "Basic interior materials", 596 | "Limited tech features", 597 | "Firm suspension", 598 | "Average build quality" 599 | ] 600 | }, 601 | { 602 | "brand": "Peugeot", 603 | "make": "Traveller", 604 | "year": 2018, 605 | "cm3": 1997, 606 | "price": 30000, 607 | "km": 30000, 608 | "description": "The Peugeot Traveller is a spacious and comfortable MPV that offers great versatility for families and businesses.", 609 | "pros": [ 610 | "Modern design", 611 | "Spacious interior", 612 | "Advanced tech features", 613 | "Comfortable ride", 614 | "Good fuel efficiency" 615 | ], 616 | "cons": [ 617 | "Higher maintenance costs", 618 | "Limited rear visibility", 619 | "Firm suspension", 620 | "Complex infotainment", 621 | "Average resale value" 622 | ] 623 | }, 624 | { 625 | "brand": "Audi", 626 | "make": "A6", 627 | "year": 2016, 628 | "cm3": 1968, 629 | "price": 27000, 630 | "km": 80000, 631 | "description": "The Audi A6 is a premium executive sedan that offers luxury, performance, and advanced technology in a stylish package.", 632 | "pros": [ 633 | "Luxurious interior", 634 | "Strong performance", 635 | "Advanced technology", 636 | "Smooth ride", 637 | "High safety rating" 638 | ], 639 | "cons": [ 640 | "Expensive maintenance", 641 | "High insurance costs", 642 | "Limited rear space", 643 | "Firm suspension", 644 | "Complex infotainment system" 645 | ] 646 | }, 647 | { 648 | "brand": "Citroen", 649 | "make": "Spacetourer", 650 | "year": 2019, 651 | "cm3": 1997, 652 | "price": 35000, 653 | "km": 20000, 654 | "description": "The Citroen Spacetourer is a spacious and comfortable MPV that offers great versatility for families and businesses.", 655 | "pros": [ 656 | "Comfortable ride", 657 | "Spacious interior", 658 | "Unique design", 659 | "Advanced tech features", 660 | "Good fuel efficiency" 661 | ], 662 | "cons": [ 663 | "Average performance", 664 | "Basic interior materials", 665 | "Limited rear visibility", 666 | "Firm suspension", 667 | "Complex infotainment" 668 | ] 669 | }, 670 | { 671 | "brand": "Volkswagen", 672 | "make": "Caddy", 673 | "year": 2017, 674 | "cm3": 1598, 675 | "price": 20000, 676 | "km": 40000, 677 | "description": "The Volkswagen Caddy is a practical and versatile van that offers great space and reliability for businesses.", 678 | "pros": [ 679 | "Solid build quality", 680 | "Comfortable ride", 681 | "Spacious interior", 682 | "Efficient engines", 683 | "Advanced tech features" 684 | ], 685 | "cons": [ 686 | "Expensive to maintain", 687 | "Average performance", 688 | "Firm suspension", 689 | "Complex infotainment", 690 | "High insurance costs" 691 | ] 692 | }, 693 | { 694 | "brand": "Ford", 695 | "make": "Transit", 696 | "year": 2018, 697 | "cm3": 1995, 698 | "price": 25000, 699 | "km": 50000, 700 | "description": "The Ford Transit is a versatile and reliable van that offers great space and practicality for businesses.", 701 | "pros": [ 702 | "Spacious interior", 703 | "Good fuel efficiency", 704 | "Practical design", 705 | "Affordable price", 706 | "Comfortable ride" 707 | ], 708 | "cons": [ 709 | "Average performance", 710 | "Basic interior materials", 711 | "Limited tech features", 712 | "Firm suspension", 713 | "Average build quality" 714 | ] 715 | }, 716 | { 717 | "brand": "Fiat", 718 | "make": "Panda", 719 | "year": 2017, 720 | "cm3": 1242, 721 | "price": 6000, 722 | "km": 80000, 723 | "description": "The Fiat Panda is a practical and efficient city car that offers great value for money and easy maneuverability.", 724 | "pros": [ 725 | "Compact size", 726 | "Good fuel economy", 727 | "Affordable price", 728 | "Easy to park", 729 | "Simple design" 730 | ], 731 | "cons": [ 732 | "Limited cargo space", 733 | "Small rear seats", 734 | "Modest performance", 735 | "Average build quality", 736 | "Highway noise" 737 | ] 738 | }, 739 | { 740 | "brand": "Peugeot", 741 | "make": "108", 742 | "year": 2018, 743 | "cm3": 998, 744 | "price": 7000, 745 | "km": 60000, 746 | "description": "The Peugeot 108 is a charming and compact city car that offers great fuel efficiency and easy maneuverability.", 747 | "pros": [ 748 | "Compact size", 749 | "Stylish design", 750 | "Good fuel economy", 751 | "Easy to park", 752 | "Affordable" 753 | ], 754 | "cons": [ 755 | "Limited cargo space", 756 | "Small rear seats", 757 | "Modest performance", 758 | "Average build quality", 759 | "Highway noise" 760 | ] 761 | }, 762 | { 763 | "brand": "Audi", 764 | "make": "A1", 765 | "year": 2016, 766 | "cm3": 999, 767 | "price": 12000, 768 | "km": 60000, 769 | "description": "The Audi A1 is a premium compact car that offers luxury, performance, and advanced technology in a small package.", 770 | "pros": [ 771 | "Luxurious interior", 772 | "Strong performance", 773 | "Advanced technology", 774 | "Smooth ride", 775 | "High safety rating" 776 | ], 777 | "cons": [ 778 | "Expensive maintenance", 779 | "High insurance costs", 780 | "Limited rear space", 781 | "Firm suspension", 782 | "Complex infotainment system" 783 | ] 784 | }, 785 | { 786 | "brand": "Citroen", 787 | "make": "C1", 788 | "year": 2017, 789 | "cm3": 998, 790 | "price": 6000, 791 | "km": 80000, 792 | "description": "The Citroen C1 is a practical and efficient city car that offers great value for money and easy maneuverability.", 793 | "pros": [ 794 | "Compact size", 795 | "Good fuel economy", 796 | "Affordable price", 797 | "Easy to park", 798 | "Simple design" 799 | ], 800 | "cons": [ 801 | "Limited cargo space", 802 | "Small rear seats", 803 | "Modest performance", 804 | "Average build quality", 805 | "Highway noise" 806 | ] 807 | }, 808 | { 809 | "brand": "Volkswagen", 810 | "make": "Up!", 811 | "year": 2018, 812 | "cm3": 999, 813 | "price": 9000, 814 | "km": 40000, 815 | "description": "The Volkswagen Up! is a compact and efficient city car that offers great fuel economy and easy maneuverability.", 816 | "pros": [ 817 | "Compact size", 818 | "Good fuel economy", 819 | "Stylish design", 820 | "Easy to park", 821 | "Affordable" 822 | ], 823 | "cons": [ 824 | "Limited cargo space", 825 | "Small rear seats", 826 | "Modest performance", 827 | "Average build quality", 828 | "Highway noise" 829 | ] 830 | }, 831 | { 832 | "brand": "Ford", 833 | "make": "Ka", 834 | "year": 2017, 835 | "cm3": 1242, 836 | "price": 7000, 837 | "km": 70000, 838 | "description": "The Ford Ka is a practical and efficient city car that offers great value for money and easy maneuverability.", 839 | "pros": [ 840 | "Compact size", 841 | "Good fuel economy", 842 | "Affordable price", 843 | "Easy to park", 844 | "Simple design" 845 | ], 846 | "cons": [ 847 | "Limited cargo space", 848 | "Small rear seats", 849 | "Modest performance", 850 | "Average build quality", 851 | "Highway noise" 852 | ] 853 | }, 854 | { 855 | "brand": "Fiat", 856 | "make": "500L", 857 | "year": 2018, 858 | "cm3": 1368, 859 | "price": 15000, 860 | "km": 40000, 861 | "description": "The Fiat 500L is a practical and spacious compact MPV that offers great value for money and a touch of Italian style.", 862 | "pros": [ 863 | "Spacious interior", 864 | "Good fuel economy", 865 | "Stylish design", 866 | "Comfortable ride", 867 | "Affordable" 868 | ], 869 | "cons": [ 870 | "Limited cargo space", 871 | "Average performance", 872 | "Basic interior materials", 873 | "Firm suspension", 874 | "Highway noise" 875 | ] 876 | } 877 | ] -------------------------------------------------------------------------------- /Chapter10/src/actions.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { getIronSession } from "iron-session" 5 | import { sessionOptions } from "./lib" 6 | import { redirect } from "next/navigation" 7 | 8 | export const getSession = async () => { 9 | 10 | const session = await getIronSession(cookies(), sessionOptions) 11 | 12 | return session 13 | 14 | } 15 | 16 | export const login = async (status, formData) => { 17 | 18 | const username = formData.get("username") 19 | const password = formData.get("password") 20 | 21 | const result = await fetch(`${process.env.API_URL}/users/login`, { 22 | 23 | method: "POST", 24 | 25 | headers: { 26 | "Content-Type": "application/json" 27 | }, 28 | 29 | body: JSON.stringify({ username, password }) 30 | 31 | }) 32 | 33 | const data = await result.json() 34 | const session = await getSession() 35 | 36 | if (result.ok) { 37 | 38 | session.username = data.username 39 | session.jwt = data.token 40 | await session.save() 41 | redirect("/private") 42 | 43 | } else { 44 | session.destroy() 45 | 46 | return { error: data.detail } 47 | } 48 | 49 | } 50 | 51 | 52 | export const logout = async () => { 53 | 54 | const session = await getSession() 55 | session.destroy() 56 | redirect("/") 57 | 58 | } 59 | 60 | export const createCar = async (state, formData) => { 61 | 62 | 63 | const session = await getSession() 64 | const jwt = session.jwt 65 | 66 | 67 | 68 | const result = await fetch(`${process.env.API_URL}/cars/`, { 69 | 70 | method: "POST", 71 | headers: { 72 | Authorization: `Bearer ${jwt}`, 73 | }, 74 | body: formData 75 | 76 | }) 77 | 78 | const data = await result.json() 79 | 80 | if (result.ok) { 81 | redirect("/") 82 | 83 | } else { 84 | return { error: data.detail } 85 | } 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /Chapter10/src/app/cars/[id]/page.js: -------------------------------------------------------------------------------- 1 | import { 2 | redirect 3 | } from "next/navigation" 4 | import Image from "next/image" 5 | 6 | export async function generateStaticParams() { 7 | const cars = await fetch( 8 | `${process.env.API_URL}/cars/`).then((res) => res.json()) 9 | return cars.map((car) => ({ id: car._id, })) 10 | } 11 | 12 | export async function generateMetadata({ params }, parent) { 13 | // read route params 14 | const carId = params.id 15 | 16 | // fetch data 17 | const car = await fetch(`${process.env.API_URL}/cars/${carId}`).then((res) => res.json()) 18 | 19 | const title = `FARM Cars App - ${car.brand} ${car.make} (${car.year})` 20 | 21 | return { 22 | title 23 | 24 | } 25 | } 26 | 27 | const CarDetails = async ({ 28 | params 29 | }) => { 30 | const carId = params.id 31 | 32 | const res = await fetch( 33 | `${process.env.API_URL}/cars/${carId}`, { 34 | next: { 35 | revalidate: 10 36 | } 37 | } 38 | ) 39 | if (!res.ok) { 40 | redirect("/error") 41 | } 42 | const data = await res.json() 43 | return ( 44 |
45 |

{data.brand} {data.make} ({data.year})

46 | 47 |

{data.description}

48 | 49 |
50 | {`${data.brand} 55 |
56 |
57 | 58 | {data.pros &&
61 |

Pros

62 |
    63 | {data.pros.map((pro, index) => ( 64 |
  1. {pro}
  2. 65 | ))} 66 |
67 |
} 68 | 69 | {data.cons &&
70 |

Cons

71 |
    72 | {data.cons.map((con, index) => ( 73 |
  1. {con}
  2. 74 | ))} 75 |
76 |
} 77 |
78 |
79 | ) 80 | } 81 | export default CarDetails 82 | -------------------------------------------------------------------------------- /Chapter10/src/app/cars/error.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | const error = () => { 3 | return ( 4 |
5 | There was an error while fetching car data! 6 |
7 | ) 8 | } 9 | export default error -------------------------------------------------------------------------------- /Chapter10/src/app/cars/layout.js: -------------------------------------------------------------------------------- 1 | const layout = ({ children }) => { 2 | return ( 3 |
4 |

Cars Layout

5 |

More common cars functionality here.

6 | {children} 7 |
8 | ) 9 | } 10 | export default layout 11 | -------------------------------------------------------------------------------- /Chapter10/src/app/cars/page.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | const Cars = async () => { 3 | 4 | const data = await fetch( 5 | `${process.env.API_URL}/cars/`, { 6 | next: { 7 | revalidate: 10 8 | } 9 | } 10 | ) 11 | const cars = await data.json() 12 | 13 | 14 | return ( 15 | <> 16 |

Cars

17 |
18 | {cars.map((car) => ( 19 |
20 | 21 |

{car.brand} {car.make} from {car.year}

22 | 23 |
24 | ))} 25 |
26 | 27 | 28 | ) 29 | } 30 | export default Cars 31 | -------------------------------------------------------------------------------- /Chapter10/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition/e4c8f4063848b81cfe76118955521c3b3d82a531/Chapter10/src/app/favicon.ico -------------------------------------------------------------------------------- /Chapter10/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /Chapter10/src/app/layout.js: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | 3 | import Navbar from "@/components/Navbar"; 4 | 5 | 6 | 7 | export const metadata = { 8 | title: "Farm Cars App", 9 | description: "Next.js + FastAPI + MongoDB App", 10 | }; 11 | 12 | export default function RootLayout({ children }) { 13 | return ( 14 | 15 | 16 | 17 |
18 | {children} 19 |
20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /Chapter10/src/app/login/page.js: -------------------------------------------------------------------------------- 1 | import LoginForm from "@/components/LoginForm" 2 | 3 | const page = () => { 4 | return ( 5 |

Login Page

6 | 7 |
8 | ) 9 | } 10 | export default page -------------------------------------------------------------------------------- /Chapter10/src/app/not-found.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | const NotFoundPage = () => { 3 | return ( 4 |
5 | 6 |

Custom Not Found Page

7 |

take a look at our cars

8 | 9 |
10 | ) 11 | } 12 | export default NotFoundPage -------------------------------------------------------------------------------- /Chapter10/src/app/page.js: -------------------------------------------------------------------------------- 1 | const Home = () => { 2 | return ( 3 |
Home
4 | ) 5 | } 6 | export default Home -------------------------------------------------------------------------------- /Chapter10/src/app/private/page.js: -------------------------------------------------------------------------------- 1 | import CarForm from "@/components/CarForm" 2 | import { getSession } from "@/actions" 3 | import { redirect } from "next/navigation" 4 | 5 | const page = async () => { 6 | 7 | const session = await getSession() 8 | 9 | if (!session?.jwt) { 10 | redirect("/login") 11 | } 12 | 13 | 14 | 15 | return ( 16 |
17 |

Private Page

18 | 19 |
20 | ) 21 | } 22 | export default page -------------------------------------------------------------------------------- /Chapter10/src/components/CarForm.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { createCar } from "@/actions" 3 | import { useFormState } from "react-dom"; 4 | 5 | import InputField from "./InputField" 6 | 7 | const CarForm = () => { 8 | 9 | let formArray = [ 10 | 11 | { 12 | name: "brand", 13 | type: "text" 14 | }, 15 | { 16 | name: "make", 17 | type: "text" 18 | }, 19 | { 20 | name: "year", 21 | type: "number" 22 | }, 23 | { 24 | name: "price", 25 | type: "number" 26 | }, 27 | { 28 | name: "km", 29 | type: "number" 30 | }, 31 | { 32 | name: "cm3", 33 | type: "number" 34 | }, 35 | { 36 | name: "picture", 37 | type: "file" 38 | } 39 | 40 | ] 41 | 42 | const [state, formAction] = useFormState(createCar, {}) 43 | return ( 44 |
45 |
{JSON.stringify(state, null, 2)}
46 |
47 |
51 |

Insert new car

52 | {formArray.map((item, index) => ( 53 | 54 | 60 | ))} 61 | 62 | 63 | 64 |
65 | 70 |
71 | 72 |
73 |
74 | ) 75 | } 76 | export default CarForm -------------------------------------------------------------------------------- /Chapter10/src/components/InputField.js: -------------------------------------------------------------------------------- 1 | 2 | const InputField = ({ props }) => { 3 | // eslint-disable-next-line react/prop-types 4 | const { name, type } = props 5 | return ( 6 | 7 |
8 | 11 | 21 |
22 | ) 23 | } 24 | export default InputField -------------------------------------------------------------------------------- /Chapter10/src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { login } from "@/actions" 3 | import { useFormState } from "react-dom"; 4 | 5 | const LoginForm = () => { 6 | 7 | const [state, formAction] = useFormState(login, {}) 8 | return ( 9 |
10 | 11 |
12 |
13 | 16 | 18 |
19 |
20 | 23 | 24 | 25 |
26 |
27 | 30 |
31 |
{JSON.stringify(state, null, 2)}
32 |
33 |
34 | ) 35 | } 36 | export default LoginForm -------------------------------------------------------------------------------- /Chapter10/src/components/LogoutForm.js: -------------------------------------------------------------------------------- 1 | import { logout } from "@/actions" 2 | 3 | const LogoutForm = () => { 4 | return ( 5 |
6 | 8 |
9 | ) 10 | } 11 | export default LogoutForm -------------------------------------------------------------------------------- /Chapter10/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import { getSession } from "@/actions"; 3 | import LogoutForm from "./LogoutForm"; 4 | 5 | const Navbar = async () => { 6 | const session = await getSession() 7 | return ( 8 | 21 | ) 22 | } 23 | export default Navbar -------------------------------------------------------------------------------- /Chapter10/src/lib.js: -------------------------------------------------------------------------------- 1 | export const sessionOptions = { 2 | password: "complex_password_at_least_32_characters_long", 3 | cookieName: "farmcars_session", 4 | cookieOptions: { 5 | httpOnly: true, 6 | secure: false, 7 | maxAge: 60 * 60, 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Chapter10/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 12 | "gradient-conic": 13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 14 | }, 15 | }, 16 | }, 17 | plugins: [ 18 | function ({ addVariant }) { 19 | addVariant('child', '& > *'); 20 | addVariant('child-hover', '& > *:hover'); 21 | } 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Packt 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition 2 | Full Stack FastAPI, React, and MongoDB 2nd Edition published by Packt 3 | --------------------------------------------------------------------------------