├── 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 | - {user.name}
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 |
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 |
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 |
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 | - {user.username}
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 |

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 |
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 |
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 |
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 |
55 |
56 |
57 |
58 | {data.pros &&
61 |
Pros
62 |
63 | {data.pros.map((pro, index) => (
64 | - {pro}
65 | ))}
66 |
67 |
}
68 |
69 | {data.cons &&
70 |
Cons
71 |
72 | {data.cons.map((con, index) => (
73 | - {con}
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 |
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 |
34 | )
35 | }
36 | export default LoginForm
--------------------------------------------------------------------------------
/Chapter10/src/components/LogoutForm.js:
--------------------------------------------------------------------------------
1 | import { logout } from "@/actions"
2 |
3 | const LogoutForm = () => {
4 | return (
5 |
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 |
--------------------------------------------------------------------------------