22 |
23 |
Your current budget is: {budget}
24 |
25 | {data.map(
26 | (el)=>{
27 |
28 | return (
29 | (el.price
30 | )
31 | }
32 | )}
33 |
34 |
35 | Budget:
36 |
37 |
38 |
39 | );
40 | }
41 | export default App;
42 |
--------------------------------------------------------------------------------
/chapter4/cars/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render(
6 |
{title}
7 |
Reprehenderit laborum sit amet aute occaecat enim nostrud cillum cupidatat commodo ad culpa nisi. Mollit excepteur amet anim adipisicing et. Dolore proident est proident anim exercitation. Cillum irure est velit consequat anim incididunt. Reprehenderit ipsum anim labore enim. In esse commodo commodo minim culpa pariatur deserunt tempor. Officia nisi labore sint aliqua duis.
8 |
9 | )
10 | }
11 |
12 | export default Lorem
--------------------------------------------------------------------------------
/chapter6/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities{
6 | .active-link{
7 | @apply bg-yellow-500 p-4 shadow-md text-white
8 | }
9 |
10 | }
--------------------------------------------------------------------------------
/chapter6/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import {
5 | BrowserRouter,
6 | Routes,
7 | Route,
8 | } from "react-router-dom";
9 |
10 |
11 |
12 | import Car from './pages/Car'
13 | import Cars from './pages/Cars'
14 | import NewCar from './pages/NewCar'
15 |
16 |
17 |
18 | import './index.css';
19 | import App from './App';
20 | import reportWebVitals from './reportWebVitals';
21 |
22 |
23 | const root = ReactDOM.createRoot(document.getElementById('root'));
24 | root.render(
25 |
26 |
4 | DB_NAME=carsApp
--------------------------------------------------------------------------------
/chapter7/backend/authentication.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | from fastapi import HTTPException, Security
3 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
4 | from passlib.context import CryptContext
5 | from datetime import datetime, timedelta
6 |
7 |
8 | class AuthHandler:
9 |
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):
21 | payload = {
22 | "exp": datetime.utcnow() + timedelta(days=0, minutes=35),
23 | "iat": datetime.utcnow(),
24 | "sub": user_id,
25 | }
26 | return jwt.encode(payload, self.secret, algorithm="HS256")
27 |
28 | def decode_token(self, token):
29 | try:
30 | payload = jwt.decode(token, self.secret, algorithms=["HS256"])
31 | return payload["sub"]
32 | except jwt.ExpiredSignatureError:
33 | raise HTTPException(status_code=401, detail="Signature has expired")
34 | except jwt.InvalidTokenError as e:
35 | raise HTTPException(status_code=401, detail="Invalid token")
36 |
37 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)):
38 | return self.decode_token(auth.credentials)
39 |
--------------------------------------------------------------------------------
/chapter7/backend/importScript.py:
--------------------------------------------------------------------------------
1 | import csv
2 | from fastapi.encoders import jsonable_encoder
3 |
4 | import random
5 |
6 |
7 |
8 | from decouple import config
9 |
10 | from models import CarBase2
11 |
12 |
13 |
14 | #
15 |
16 | # read csv
17 | with open("sample_data.csv",encoding='utf-8') as f:
18 | csv_reader = csv.DictReader(f)
19 | name_records = list(csv_reader)
20 |
21 |
22 |
23 | # Mongo db - we do not need Motor here
24 | from pymongo import MongoClient
25 | client = MongoClient()
26 |
27 | DB_URL = config('DB_URL', cast=str)
28 | DB_NAME = config('DB_NAME', cast=str)
29 |
30 |
31 | client = MongoClient(DB_URL)
32 | db = client[DB_NAME]
33 | cars = db['cars2']
34 |
35 | users = db['users']
36 |
37 |
38 | ids_list = []
39 | all_users=users.find({})
40 |
41 | for x in all_users:
42 | ids_list.append(x['_id'])
43 |
44 | print(ids_list)
45 | for rec in name_records[1:250]:
46 |
47 | try:
48 | rec['cm3'] = int(rec['cm3'])
49 | rec['price'] = int(rec['price'])
50 | rec['year'] = int(rec['year'])
51 | rec['owner'] = random.choice(ids_list)
52 |
53 | if rec['price']>1000:
54 | car = jsonable_encoder(CarBase2(**rec))
55 | print("Inserting:",car)
56 | cars.insert_one(car)
57 |
58 | except ValueError:
59 | pass
60 |
--------------------------------------------------------------------------------
/chapter7/backend/main.py:
--------------------------------------------------------------------------------
1 | from decouple import config
2 |
3 | from fastapi import FastAPI
4 | from fastapi.middleware.cors import CORSMiddleware
5 |
6 | from motor.motor_asyncio import AsyncIOMotorClient
7 |
8 | from routers.cars import router as cars_router
9 | from routers.users import router as users_router
10 |
11 |
12 | DB_URL = config('DB_URL', cast=str)
13 | DB_NAME = config('DB_NAME', cast=str)
14 |
15 |
16 | # define origins
17 | origins = [
18 | "*"
19 | ]
20 |
21 | # instantiate the app
22 | app = FastAPI()
23 |
24 | # add CORS middleware
25 | app.add_middleware(
26 | CORSMiddleware,
27 | allow_origins=origins,
28 | allow_credentials=True,
29 | allow_methods=["*"],
30 | allow_headers=["*"]
31 | )
32 |
33 | @app.on_event("startup")
34 | async def startup_db_client():
35 | app.mongodb_client = AsyncIOMotorClient(DB_URL)
36 | app.mongodb = app.mongodb_client[DB_NAME]
37 |
38 | @app.on_event("shutdown")
39 | async def shutdown_db_client():
40 | app.mongodb_client.close()
41 |
42 | app.include_router(cars_router, prefix="/cars", tags=["cars"])
43 | app.include_router(users_router, prefix="/users", tags=["users"])
--------------------------------------------------------------------------------
/chapter7/backend/models.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from bson import ObjectId
3 | from typing import Optional
4 |
5 | from pydantic import EmailStr, Field, BaseModel, validator
6 |
7 | from email_validator import validate_email, EmailNotValidError
8 |
9 |
10 | class PyObjectId(ObjectId):
11 | @classmethod
12 | def __get_validators__(cls):
13 | yield cls.validate
14 |
15 | @classmethod
16 | def validate(cls, v):
17 | if not ObjectId.is_valid(v):
18 | raise ValueError("Invalid objectid")
19 | return ObjectId(v)
20 |
21 | @classmethod
22 | def __modify_schema__(cls, field_schema):
23 | field_schema.update(type="string")
24 |
25 |
26 | class MongoBaseModel(BaseModel):
27 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
28 |
29 | class Config:
30 | json_encoders = {ObjectId: str}
31 |
32 |
33 | class Role(str, Enum):
34 |
35 | SALESMAN = "SALESMAN"
36 | ADMIN = "ADMIN"
37 |
38 |
39 | class UserBase(MongoBaseModel):
40 |
41 | username: str = Field(..., min_length=3, max_length=15)
42 | email: str = Field(...)
43 | password: str = Field(...)
44 | role: Role
45 |
46 | @validator("email")
47 | def valid_email(cls, v):
48 |
49 | try:
50 | email = validate_email(v).email
51 | return email
52 | except EmailNotValidError as e:
53 |
54 | raise EmailNotValidError
55 |
56 |
57 | class LoginBase(BaseModel):
58 | email: str = EmailStr(...)
59 | password: str = Field(...)
60 |
61 |
62 | class CurrentUser(BaseModel):
63 | email: str = EmailStr(...)
64 | username: str = Field(...)
65 | role: str = Field(...)
66 |
67 |
68 | class CarBase(MongoBaseModel):
69 |
70 | brand: str = Field(..., min_length=3)
71 | make: str = Field(..., min_length=1)
72 | year: int = Field(..., gt=1975, lt=2023)
73 | price: int = Field(...)
74 | km: int = Field(...)
75 | cm3: int = Field(..., gt=600, lt=8000)
76 |
77 |
78 | class CarDB(CarBase):
79 | owner: str = Field(...)
80 |
81 |
82 | class CarUpdate(MongoBaseModel):
83 | price: Optional[int] = None
84 |
--------------------------------------------------------------------------------
/chapter7/backend/reqs.txt:
--------------------------------------------------------------------------------
1 | anyio==3.5.0
2 | asgiref==3.5.0
3 | backports.entry-points-selectable==1.1.1
4 | bcrypt==3.2.2
5 | bson==0.5.10
6 | certifi==2021.10.8
7 | cffi==1.15.0
8 | charset-normalizer==2.0.12
9 | click==8.1.2
10 | colorama==0.4.4
11 | cryptography==37.0.2
12 | defusedxml==0.7.1
13 | Deprecated==1.2.13
14 | distlib==0.3.4
15 | dnslib==0.9.19
16 | dnspython==2.2.1
17 | email-validator==1.2.1
18 | fastapi==0.75.2
19 | filelock==3.4.0
20 | h11==0.13.0
21 | httpie==3.1.0
22 | idna==3.3
23 | jwcrypto==1.3.1
24 | motor==2.5.1
25 | multidict==6.0.2
26 | passlib==1.7.4
27 | pipenv==2021.11.23
28 | platformdirs==2.4.0
29 | pycparser==2.21
30 | pydantic==1.9.0
31 | Pygments==2.12.0
32 | PyJWT==2.4.0
33 | pymongo==3.12.3
34 | PySocks==1.7.1
35 | python-dateutil==2.8.2
36 | python-decouple==3.6
37 | python-dotenv==0.20.0
38 | python-jwt==3.3.2
39 | PythonDNS==0.1
40 | requests==2.27.1
41 | requests-toolbelt==0.9.1
42 | six==1.16.0
43 | sniffio==1.2.0
44 | starlette==0.17.1
45 | typing_extensions==4.2.0
46 | urllib3==1.26.9
47 | uvicorn==0.17.6
48 | virtualenv==20.10.0
49 | virtualenv-clone==0.5.7
50 | wrapt==1.14.1
51 |
--------------------------------------------------------------------------------
/chapter7/backend/routers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/backend/routers/__init__.py
--------------------------------------------------------------------------------
/chapter7/backend/routers/cars.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 |
3 | from fastapi import APIRouter, Request, Body, status, HTTPException, Depends
4 | from fastapi.encoders import jsonable_encoder
5 | from fastapi.responses import JSONResponse
6 |
7 | from models import CarBase, CarDB, CarUpdate
8 |
9 | from authentication import AuthHandler
10 |
11 | router = APIRouter()
12 |
13 | # instantiate the Auth Handler
14 | auth_handler = AuthHandler()
15 |
16 |
17 | @router.get("/", response_description="List all cars")
18 | async def list_all_cars(
19 | request: Request,
20 | min_price: int = 0,
21 | max_price: int = 100000,
22 | brand: Optional[str] = None,
23 | page: int = 1,
24 | userId=Depends(auth_handler.auth_wrapper),
25 | ) -> List[CarDB]:
26 |
27 | RESULTS_PER_PAGE = 25
28 | skip = (page - 1) * RESULTS_PER_PAGE
29 |
30 | query = {"price": {"$lt": max_price, "$gt": min_price}}
31 | if brand:
32 | query["brand"] = brand
33 |
34 | full_query = (
35 | request.app.mongodb["cars2"]
36 | .find(query)
37 | .sort("_id", -1)
38 | .skip(skip)
39 | .limit(RESULTS_PER_PAGE)
40 | )
41 |
42 | results = [CarDB(**raw_car) async for raw_car in full_query]
43 |
44 | return results
45 |
46 |
47 | # create new car
48 | @router.post("/", response_description="Add new car")
49 | async def create_car(
50 | request: Request,
51 | car: CarBase = Body(...),
52 | userId=Depends(auth_handler.auth_wrapper),
53 | ):
54 |
55 | car = jsonable_encoder(car)
56 | car["owner"] = userId
57 |
58 | new_car = await request.app.mongodb["cars2"].insert_one(car)
59 | created_car = await request.app.mongodb["cars2"].find_one(
60 | {"_id": new_car.inserted_id}
61 | )
62 |
63 | return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_car)
64 |
65 |
66 | # get car by ID
67 | @router.get("/{id}", response_description="Get a single car")
68 | async def show_car(id: str, request: Request):
69 | if (car := await request.app.mongodb["cars2"].find_one({"_id": id})) is not None:
70 | return CarDB(**car)
71 | raise HTTPException(status_code=404, detail=f"Car with {id} not found")
72 |
73 |
74 | @router.patch("/{id}", response_description="Update car")
75 | async def update_task(
76 | id: str,
77 | request: Request,
78 | car: CarUpdate = Body(...),
79 | userId=Depends(auth_handler.auth_wrapper),
80 | ):
81 |
82 | # check if the user trying to modify is an admin:
83 | user = await request.app.mongodb["users"].find_one({"_id": userId})
84 |
85 | # check if the car is owned by the user trying to modify it
86 | findCar = await request.app.mongodb["cars2"].find_one({"_id": id})
87 |
88 | if (findCar["owner"] != userId) and user["role"] != "ADMIN":
89 | raise HTTPException(
90 | status_code=401, detail="Only the owner or an admin can update the car"
91 | )
92 |
93 | await request.app.mongodb["cars2"].update_one(
94 | {"_id": id}, {"$set": car.dict(exclude_unset=True)}
95 | )
96 |
97 | if (car := await request.app.mongodb["cars2"].find_one({"_id": id})) is not None:
98 | return CarDB(**car)
99 |
100 | raise HTTPException(status_code=404, detail=f"Car with {id} not found")
101 |
102 |
103 | @router.delete("/{id}", response_description="Delete car")
104 | async def delete_task(
105 | id: str, request: Request, userId=Depends(auth_handler.auth_wrapper)
106 | ):
107 |
108 | # check if the car is owned by the user trying to delete it
109 | findCar = await request.app.mongodb["cars2"].find_one({"_id": id})
110 |
111 | if findCar["owner"] != userId:
112 | raise HTTPException(status_code=401, detail="Only the owner can delete the car")
113 |
114 | delete_result = await request.app.mongodb["cars2"].delete_one({"_id": id})
115 |
116 | if delete_result.deleted_count == 1:
117 | return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
118 |
119 | raise HTTPException(status_code=404, detail=f"Car with {id} not found")
120 |
121 |
122 | # optional
123 | @router.get("/brand/{brand}", response_description="Get brand overview")
124 | async def brand_price(brand: str, request: Request):
125 |
126 | query = [
127 | {"$match": {"brand": brand}},
128 | {"$project": {"_id": 0, "price": 1, "year": 1, "make": 1}},
129 | {
130 | "$group": {"_id": {"model": "$make"}, "avgPrice": {"$avg": "$price"}},
131 | },
132 | {"$sort": {"avgPrice": 1}},
133 | ]
134 |
135 | full_query = request.app.mongodb["cars2"].aggregate(query)
136 |
137 | results = [el async for el in full_query]
138 |
139 | return results
140 |
--------------------------------------------------------------------------------
/chapter7/backend/routers/users.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, Body, status, HTTPException, Depends
2 | from fastapi.encoders import jsonable_encoder
3 | from fastapi.responses import JSONResponse
4 |
5 | from models import UserBase, LoginBase, CurrentUser
6 |
7 | from authentication import AuthHandler
8 |
9 | router = APIRouter()
10 |
11 | # instantiate the Auth Handler
12 | auth_handler = AuthHandler()
13 |
14 | # register user
15 | # validate the data and create a user if the username and the email are valid and available
16 |
17 |
18 | @router.post("/register", response_description="Register user")
19 | async def register(request: Request, newUser: UserBase = Body(...)) -> UserBase:
20 |
21 | # hash the password before inserting it into MongoDB
22 | newUser.password = auth_handler.get_password_hash(newUser.password)
23 |
24 | newUser = jsonable_encoder(newUser)
25 |
26 | # check existing user or email 409 Conflict:
27 | if (
28 | existing_email := await request.app.mongodb["users"].find_one(
29 | {"email": newUser["email"]}
30 | )
31 | is not None
32 | ):
33 | raise HTTPException(
34 | status_code=409, detail=f"User with email {newUser['email']} already exists"
35 | )
36 |
37 | # check existing user or email 409 Conflict:
38 | if (
39 | existing_username := await request.app.mongodb["users"].find_one(
40 | {"username": newUser["username"]}
41 | )
42 | is not None
43 | ):
44 | raise HTTPException(
45 | status_code=409,
46 | detail=f"User with username {newUser['username']} already exists",
47 | )
48 |
49 | user = await request.app.mongodb["users"].insert_one(newUser)
50 | created_user = await request.app.mongodb["users"].find_one(
51 | {"_id": user.inserted_id}
52 | )
53 |
54 | return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_user)
55 |
56 |
57 | # post user
58 | @router.post("/login", response_description="Login user")
59 | async def login(request: Request, loginUser: LoginBase = Body(...)) -> str:
60 |
61 | # find the user by email
62 | user = await request.app.mongodb["users"].find_one({"email": loginUser.email})
63 |
64 | # check password
65 | if (user is None) or (
66 | not auth_handler.verify_password(loginUser.password, user["password"])
67 | ):
68 | raise HTTPException(status_code=401, detail="Invalid email and/or password")
69 | token = auth_handler.encode_token(user["_id"])
70 | response = JSONResponse(content={"token": token})
71 |
72 | return response
73 |
74 |
75 | # me route
76 | @router.get("/me", response_description="Logged in user data")
77 | async def me(request: Request, userId=Depends(auth_handler.auth_wrapper)):
78 |
79 | currentUser = await request.app.mongodb["users"].find_one({"_id": userId})
80 | result = CurrentUser(**currentUser).dict()
81 | result["id"] = userId
82 |
83 | return JSONResponse(status_code=status.HTTP_200_OK, content=result)
84 |
--------------------------------------------------------------------------------
/chapter7/frontend/.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 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/chapter7/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/chapter7/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.4",
7 | "@testing-library/react": "^13.2.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "cookie-parser": "^1.4.6",
10 | "daisyui": "^2.15.0",
11 | "react": "^18.1.0",
12 | "react-dom": "^18.1.0",
13 | "react-hook-form": "^7.31.2",
14 | "react-router-dom": "^6.3.0",
15 | "react-scripts": "5.0.1",
16 | "web-vitals": "^2.1.4"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": [
26 | "react-app",
27 | "react-app/jest"
28 | ]
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | },
42 | "devDependencies": {
43 | "autoprefixer": "^10.4.7",
44 | "postcss": "^8.4.14",
45 | "tailwindcss": "^3.0.24"
46 | },
47 | "proxy": "http://127.0.0.1:8000"
48 | }
49 |
--------------------------------------------------------------------------------
/chapter7/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/chapter7/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/chapter7/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/chapter7/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/logo192.png
--------------------------------------------------------------------------------
/chapter7/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/logo512.png
--------------------------------------------------------------------------------
/chapter7/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/chapter7/frontend/public/pexels-johnmark-smith-280783.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/pexels-johnmark-smith-280783.jpg
--------------------------------------------------------------------------------
/chapter7/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/App.js:
--------------------------------------------------------------------------------
1 |
2 | import {Route, Routes} from "react-router-dom"
3 |
4 |
5 | import Layout from "./components/Layout";
6 | import HomePage from "./components/HomePage";
7 | import Login from "./components/Login";
8 | import Register from "./components/Register";
9 | import Protected from "./components/Protected";
10 | import Admin from "./components/Admin";
11 | import NotFound from "./components/NotFound";
12 | import NewCar from "./components/NewCar";
13 | import RequireAuthentication from "./components/RequireAuthentication";
14 |
15 | function App() {
16 |
17 | return (
18 |
19 | }>
20 | } />
21 | } />
22 | } />
23 |
24 | }>
25 | } />
26 | } />
27 | } />
28 | } />
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default App;
36 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Admin.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import useAuth from '../hooks/useAuth'
3 |
4 | const Admin = () => {
5 | const {auth} = useAuth()
6 |
7 |
8 |
9 | return (
10 |
11 | {auth?.role==="ADMIN"?
12 | Ok Admin! You are {auth.username} and you seem to be an Admin.
13 | :Only admins, sorry }
14 |
15 |
16 | )
17 | }
18 |
19 | export default Admin
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Card.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Card = ({ car }) => {
4 | let { brand, price, make, year, km, cm3 } = car;
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | {brand} {make}
13 |
14 |
15 |
16 | Year: {year} / Cm3: {cm3} / Km: {km}
17 |
18 |
19 | Price: {price} {" "}
20 | EURO
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default Card;
29 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Footer = () => {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export default Footer
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link, useNavigate } from 'react-router-dom'
3 | import useAuth from '../hooks/useAuth'
4 |
5 |
6 | const Header = () => {
7 |
8 | const {auth,setAuth} = useAuth()
9 | let navigate = useNavigate();
10 |
11 | const logout = () =>{
12 | setAuth({})
13 | navigate("/login", {replace:true})
14 |
15 |
16 | }
17 |
18 | return (
19 |
20 |
21 | FARM Cars
22 |
23 | {auth?.username?`Logged in as ${auth?.username} - ${auth.role}`:"Not logged in"}
24 |
25 |
26 |
27 |
28 | {!auth?.username &&
29 | Login }
30 | {!auth?.username &&
31 | Register }
32 | Admin
33 | Protected
34 | New Car
35 | {auth?.username &&
36 |
37 | Logout {auth?.username}
38 | }
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export default Header
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const HomePage = () => {
4 | return (
5 |
9 |
10 |
11 |
12 |
13 |
FARM Stack Cars App!
14 |
FastAPI + MongoDB + React and some really affordable cars.
15 |
Get Started
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default HomePage
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Outlet } from 'react-router-dom'
4 |
5 | import Header from './Header'
6 | import Footer from './Footer'
7 |
8 |
9 | const Layout = () => {
10 |
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default Layout
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { useState } from "react";
3 | import { useNavigate } from "react-router-dom";
4 |
5 | import useAuth from "../hooks/useAuth";
6 |
7 | const Login = () => {
8 | const [apiError, setApiError] = useState();
9 |
10 | const { setAuth } = useAuth();
11 |
12 | let navigate = useNavigate();
13 |
14 | const {
15 | register,
16 | handleSubmit,
17 | formState: { errors },
18 | } = useForm();
19 |
20 | const getUserData = async (token) => {
21 | const response = await fetch("http://127.0.0.1:8000/users/me", {
22 | method: "GET",
23 | headers: {
24 | "Content-Type": "application/json",
25 | Authorization: `Bearer ${token}`,
26 | },
27 | });
28 | if (response.ok) {
29 | let userData = await response.json();
30 | console.log(userData);
31 | userData["token"] = token;
32 | setAuth(userData);
33 | setApiError(null);
34 | navigate("/", { replace: true });
35 | }
36 | };
37 |
38 | const onFormSubmit = async (data) => {
39 | const response = await fetch("/users/login", {
40 | method: "POST",
41 | headers: {
42 | "Content-Type": "application/json",
43 | },
44 | body: JSON.stringify(data),
45 | });
46 |
47 | // if the login is successful - get the token and then get the remaining data from the /me route
48 | if (response.ok) {
49 | const token = await response.json();
50 | await getUserData(token["token"]);
51 | } else {
52 | let errorResponse = await response.json();
53 | setApiError(errorResponse["detail"]);
54 | setAuth(null);
55 | }
56 | };
57 |
58 | const onErrors = (errors) => console.error(errors);
59 |
60 | return (
61 |
62 |
63 | Login page
64 |
65 |
66 |
92 |
93 | {apiError && (
94 |
95 |
96 |
102 |
108 |
109 |
{apiError}
110 |
111 |
112 | )}
113 |
114 | );
115 | };
116 |
117 | export default Login;
118 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const NotFound = () => {
4 | return (
5 | NotFound
6 | )
7 | }
8 |
9 | export default NotFound
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Protected.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useEffect, useState } from "react";
3 |
4 | import useAuth from "../hooks/useAuth";
5 |
6 | import Card from "./Card";
7 |
8 | const Protected = () => {
9 | const { auth } = useAuth();
10 |
11 | const [cars, setCars] = useState([]);
12 |
13 | useEffect(() => {
14 | fetch("http://127.0.0.1:8000/cars/", {
15 | method: "GET",
16 | headers: {
17 | "Content-Type": "application/json",
18 | Authorization: `Bearer ${auth.token}`,
19 | },
20 | })
21 | .then((response) => response.json())
22 | .then((json) => {
23 | setCars(json);
24 | });
25 | }, [auth.token]);
26 |
27 | return (
28 |
29 |
30 | Cars Page
31 |
32 |
33 |
34 | {cars &&
35 | cars.map((el) => {
36 | return ;
37 | })}
38 |
39 |
40 | );
41 | };
42 |
43 | export default Protected;
44 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/Register.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Register = () => {
4 | return (
5 |
6 |
Register
7 |
Registration is not implemented on the client side.
8 |
9 | )
10 | }
11 |
12 | export default Register
--------------------------------------------------------------------------------
/chapter7/frontend/src/components/RequireAuthentication.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Outlet } from "react-router-dom";
2 | import useAuth from "../hooks/useAuth";
3 |
4 | const RequireAuthentication = () => {
5 | const { auth } = useAuth();
6 |
7 | return auth?.username ? : ;
8 | };
9 |
10 | export default RequireAuthentication;
11 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/context/AuthProvider.js:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from "react";
2 |
3 | const AuthContext = createContext({
4 |
5 | })
6 |
7 | export const AuthProvider = ({children}) => {
8 | const [auth, setAuth] = useState({
9 |
10 | })
11 | return
12 | {children}
13 |
14 | }
15 |
16 |
17 | export default AuthContext
--------------------------------------------------------------------------------
/chapter7/frontend/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import AuthContext from "../context/AuthProvider";
3 |
4 | const useAuth = () => {
5 | return useContext(AuthContext)
6 | }
7 |
8 | export default useAuth;
--------------------------------------------------------------------------------
/chapter7/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/chapter7/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import {BrowserRouter, Routes, Route} from "react-router-dom"
5 | import App from './App';
6 | import { AuthProvider } from './context/AuthProvider';
7 |
8 |
9 | const root = ReactDOM.createRoot(document.getElementById('root'));
10 | root.render(
11 |
12 |
13 |
14 |
15 | } />
16 |
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/chapter7/frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/chapter7/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./src/**/*.{js,jsx,ts,tsx}",
4 | ],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [require("daisyui")],
9 | }
--------------------------------------------------------------------------------
/chapter8/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105 | __pypackages__/
106 |
107 | # Celery stuff
108 | celerybeat-schedule
109 | celerybeat.pid
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | ENV/
120 | env.bak/
121 | venv.bak/
122 |
123 | # Spyder project settings
124 | .spyderproject
125 | .spyproject
126 |
127 | # Rope project settings
128 | .ropeproject
129 |
130 | # mkdocs documentation
131 | /site
132 |
133 | # mypy
134 | .mypy_cache/
135 | .dmypy.json
136 | dmypy.json
137 |
138 | # Pyre type checker
139 | .pyre/
140 |
141 | # pytype static type analyzer
142 | .pytype/
143 |
144 | # Cython debug symbols
145 | cython_debug/
146 |
147 | # PyCharm
148 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150 | # and can be added to the global gitignore or merged into this file. For a more nuclear
151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152 | #.idea/
--------------------------------------------------------------------------------
/chapter8/backend/Procfile:
--------------------------------------------------------------------------------
1 | web: uvicorn main:app --host=0.0.0.0 --port=${PORT:-5000}
--------------------------------------------------------------------------------
/chapter8/backend/authentication.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | from fastapi import HTTPException, Security
3 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
4 | from passlib.context import CryptContext
5 | from datetime import datetime, timedelta
6 |
7 |
8 | class AuthHandler:
9 |
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):
21 | payload = {
22 | "exp": datetime.utcnow() + timedelta(days=0, minutes=30),
23 | "iat": datetime.utcnow(),
24 | "sub": user_id,
25 | }
26 | return jwt.encode(payload, self.secret, algorithm="HS256")
27 |
28 | def decode_token(self, token):
29 | try:
30 | payload = jwt.decode(token, self.secret, algorithms=["HS256"])
31 | return payload["sub"]
32 | except jwt.ExpiredSignatureError:
33 | raise HTTPException(status_code=401, detail="Signature has expired")
34 | except jwt.InvalidTokenError as e:
35 | raise HTTPException(status_code=401, detail="Invalid token")
36 |
37 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)):
38 | return self.decode_token(auth.credentials)
39 |
--------------------------------------------------------------------------------
/chapter8/backend/main.py:
--------------------------------------------------------------------------------
1 | from decouple import config
2 |
3 | from fastapi import FastAPI
4 |
5 | # there seems to be a bug with FastAPI's middleware
6 | # https://stackoverflow.com/questions/65191061/fastapi-cors-middleware-not-working-with-get-method/65994876#65994876
7 |
8 | from starlette.middleware import Middleware
9 | from starlette.middleware.cors import CORSMiddleware
10 |
11 |
12 | middleware = [
13 | Middleware(
14 | CORSMiddleware,
15 | allow_origins=["*"],
16 | allow_credentials=True,
17 | allow_methods=["*"],
18 | allow_headers=["*"],
19 | )
20 | ]
21 |
22 | from motor.motor_asyncio import AsyncIOMotorClient
23 |
24 | from routers.cars import router as cars_router
25 | from routers.users import router as users_router
26 |
27 |
28 | DB_URL = config("DB_URL", cast=str)
29 | DB_NAME = config("DB_NAME", cast=str)
30 |
31 |
32 | # define origins
33 | origins = [
34 | "*",
35 | ]
36 |
37 | # instantiate the app
38 | app = FastAPI(middleware=middleware)
39 |
40 | app.include_router(cars_router, prefix="/cars", tags=["cars"])
41 | app.include_router(users_router, prefix="/users", tags=["users"])
42 |
43 |
44 | @app.on_event("startup")
45 | async def startup_db_client():
46 | app.mongodb_client = AsyncIOMotorClient(DB_URL)
47 | app.mongodb = app.mongodb_client[DB_NAME]
48 |
49 |
50 | @app.on_event("shutdown")
51 | async def shutdown_db_client():
52 | app.mongodb_client.close()
53 |
--------------------------------------------------------------------------------
/chapter8/backend/models.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from bson import ObjectId
3 | from typing import Optional
4 |
5 | from pydantic import EmailStr, Field, BaseModel, validator
6 |
7 | from email_validator import validate_email, EmailNotValidError
8 |
9 |
10 | class PyObjectId(ObjectId):
11 | @classmethod
12 | def __get_validators__(cls):
13 | yield cls.validate
14 |
15 | @classmethod
16 | def validate(cls, v):
17 | if not ObjectId.is_valid(v):
18 | raise ValueError("Invalid objectid")
19 | return ObjectId(v)
20 |
21 | @classmethod
22 | def __modify_schema__(cls, field_schema):
23 | field_schema.update(type="string")
24 |
25 |
26 | class MongoBaseModel(BaseModel):
27 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
28 |
29 | class Config:
30 | json_encoders = {ObjectId: str}
31 |
32 |
33 | class Role(str, Enum):
34 |
35 | SALESMAN = "SALESMAN"
36 | ADMIN = "ADMIN"
37 |
38 |
39 | class UserBase(MongoBaseModel):
40 |
41 | username: str = Field(..., min_length=3, max_length=15)
42 | email: str = Field(...)
43 | password: str = Field(...)
44 | role: Role
45 |
46 | @validator("email")
47 | def valid_email(cls, v):
48 |
49 | try:
50 | email = validate_email(v).email
51 | return email
52 | except EmailNotValidError as e:
53 |
54 | raise EmailNotValidError
55 |
56 |
57 | class LoginBase(BaseModel):
58 | email: str = EmailStr(...)
59 | password: str = Field(...)
60 |
61 |
62 | class CurrentUser(BaseModel):
63 | email: str = EmailStr(...)
64 | username: str = Field(...)
65 | role: str = Field(...)
66 |
67 |
68 | class CarBase(MongoBaseModel):
69 |
70 | brand: str = Field(..., min_length=3)
71 | make: str = Field(..., min_length=1)
72 | year: int = Field(..., gt=1975, lt=2023)
73 | price: int = Field(...)
74 | km: int = Field(...)
75 | cm3: int = Field(..., gt=600, lt=8000)
76 | picture: Optional[str] = None
77 |
78 |
79 | class CarDB(CarBase):
80 | owner: str = Field(...)
81 |
82 |
83 | class CarUpdate(MongoBaseModel):
84 | price: Optional[int] = None
85 |
--------------------------------------------------------------------------------
/chapter8/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | bcrypt
2 | cloudinary
3 | colorama
4 | cryptography
5 | defusedxml
6 | Deprecated
7 | distlib
8 | dnslib
9 | dnspython
10 | email-validator
11 | fastapi
12 | idna
13 | jwcrypto
14 | motor
15 | multidict
16 | passlib
17 | Pillow
18 | pipenv
19 | pycparser
20 | pydantic
21 | PyJWT
22 | pymongo
23 | PySocks
24 | python-dateutil
25 | python-decouple
26 | python-dotenv
27 | python-jwt
28 | python-multipart
29 | PythonDNS
30 | requests
31 | uvicorn
32 | virtualenv
33 | virtualenv-clone
--------------------------------------------------------------------------------
/chapter8/backend/routers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter8/backend/routers/__init__.py
--------------------------------------------------------------------------------
/chapter8/backend/routers/users.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, Body, status, HTTPException, Depends
2 | from fastapi.encoders import jsonable_encoder
3 | from fastapi.responses import JSONResponse
4 |
5 | from models import UserBase, LoginBase, CurrentUser
6 |
7 | from authentication import AuthHandler
8 |
9 | router = APIRouter()
10 |
11 | # instantiate the Auth Handler
12 | auth_handler = AuthHandler()
13 |
14 | # register user
15 | # validate the data and create a user if the username and the email are valid and available
16 |
17 |
18 | @router.post("/register", response_description="Register user")
19 | async def register(request: Request, newUser: UserBase = Body(...)) -> UserBase:
20 |
21 | # hash the password before inserting it into MongoDB
22 | newUser.password = auth_handler.get_password_hash(newUser.password)
23 |
24 | newUser = jsonable_encoder(newUser)
25 |
26 | # check existing user or email 409 Conflict:
27 | if (
28 | existing_email := await request.app.mongodb["users"].find_one(
29 | {"email": newUser["email"]}
30 | )
31 | is not None
32 | ):
33 | raise HTTPException(
34 | status_code=409, detail=f"User with email {newUser['email']} already exists"
35 | )
36 |
37 | # check existing user or email 409 Conflict:
38 | if (
39 | existing_username := await request.app.mongodb["users"].find_one(
40 | {"username": newUser["username"]}
41 | )
42 | is not None
43 | ):
44 | raise HTTPException(
45 | status_code=409,
46 | detail=f"User with username {newUser['username']} already exists",
47 | )
48 |
49 | user = await request.app.mongodb["users"].insert_one(newUser)
50 | created_user = await request.app.mongodb["users"].find_one(
51 | {"_id": user.inserted_id}
52 | )
53 |
54 | return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_user)
55 |
56 |
57 | # post user
58 | @router.post("/login", response_description="Login user")
59 | async def login(request: Request, loginUser: LoginBase = Body(...)) -> str:
60 |
61 | # find the user by email
62 | user = await request.app.mongodb["users"].find_one({"email": loginUser.email})
63 |
64 | # check password
65 | if (user is None) or (
66 | not auth_handler.verify_password(loginUser.password, user["password"])
67 | ):
68 | raise HTTPException(status_code=401, detail="Invalid email and/or password")
69 | token = auth_handler.encode_token(user["_id"])
70 | response = JSONResponse(
71 | content={"token": token, "user": CurrentUser(**user).dict()}
72 | )
73 |
74 | return response
75 |
76 |
77 | # me route
78 | @router.get("/me", response_description="Logged in user data")
79 | async def me(request: Request, userId=Depends(auth_handler.auth_wrapper)):
80 |
81 | currentUser = await request.app.mongodb["users"].find_one({"_id": userId})
82 | result = CurrentUser(**currentUser).dict()
83 | result["id"] = userId
84 |
85 | return JSONResponse(status_code=status.HTTP_200_OK, content=result)
86 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/.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 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/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 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | 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.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/components/Card.js:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import Link from 'next/link'
3 | import { buildUrl } from 'cloudinary-build-url'
4 |
5 |
6 | const transformedUrl = (id)=> buildUrl(id, {
7 | cloud: {
8 | cloudName: 'ddyjlwyjv',
9 | },
10 |
11 | transformations: {
12 | effect: {
13 | name: 'grayscale'
14 | },
15 | effect: {
16 | name: 'tint',
17 | value: '60:blue:white',
18 |
19 | }
20 | }
21 | });
22 |
23 | const Card = ({brand, make, year, url, km, price, cm3, id}) => {
24 | return (
25 |
26 |
27 |
28 |
29 |
{brand} {make}
30 |
Price: {price} EUR
31 |
32 | A detailed car description from the Cars FARM crew.
33 |
34 |
35 |
36 | made in {year}
37 | Cm3:{cm3}
38 | Km:{km}
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export default Card
46 |
47 |
48 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | const Footer = () => {
2 | return Footer
;
3 | };
4 |
5 | export default Footer;
6 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import useAuth from "../hooks/useAuth";
3 | import { useEffect } from "react";
4 |
5 | const Header = () => {
6 | const { user, setUser, loading, setLoading } = useAuth();
7 | useEffect(() => {
8 | (async () => {
9 | const userData = await fetch("/api/user");
10 | try {
11 | const user = await userData.json();
12 |
13 | setUser(user);
14 | } catch (error) {
15 | // if error: set user to null, destroy the cookie
16 | setUser(null);
17 | }
18 | })();
19 | }, []);
20 | return (
21 |
22 |
37 |
38 |
39 |
40 | Cars
41 |
42 |
43 | {user && user.role === "ADMIN" ? (
44 |
45 |
46 | Add Car
47 |
48 |
49 | ) : (
50 | ""
51 | )}
52 |
53 | {!user ? (
54 | <>
55 |
56 |
57 | Register
58 |
59 |
60 |
61 |
62 | Login
63 |
64 |
65 | >
66 | ) : (
67 | <>
68 |
69 |
70 | Log out {user.username}
71 |
72 |
73 | >
74 | )}
75 |
76 |
77 | );
78 | };
79 | export default Header;
80 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/context/AuthContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from "react";
2 |
3 | const AuthContext = createContext({})
4 |
5 | export const AuthProvider = ({children}) => {
6 | const [user, setUser] = useState(null)
7 | const [authError, setAuthError] = useState(null)
8 | const [loading, setLoading] = useState(false)
9 |
10 | return
11 | {children}
12 |
13 | }
14 |
15 | export default AuthContext
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import AuthContext from "../context/AuthContext";
3 |
4 | const useAuth = () => {
5 | return useContext(AuthContext)
6 | }
7 |
8 | export default useAuth;
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/middleware.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 |
4 | export function middleware(req){
5 |
6 | const url = req.url
7 | const cookie = req.cookies.get('jwt')
8 |
9 | if(url.includes('/cars/add') && (cookie===undefined)){
10 | return NextResponse.redirect('http://localhost:3000/account/login')
11 | }
12 | return NextResponse.next()
13 | }
14 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | images: {
3 | domains: ['res.cloudinary.com'],
4 | },
5 | }
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-cars",
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 | "axios": "^0.27.2",
13 | "cloudinary-build-url": "^0.2.4",
14 | "cookie": "^0.5.0",
15 | "cookies-next": "^2.1.1",
16 | "jsonwebtoken": "^8.5.1",
17 | "jwt-decode": "^3.1.2",
18 | "next": "12.2.0",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0",
21 | "swr": "^1.3.0"
22 | },
23 | "devDependencies": {
24 | "@tailwindcss/forms": "^0.5.2",
25 | "autoprefixer": "^10.4.7",
26 | "eslint": "8.19.0",
27 | "eslint-config-next": "12.2.0",
28 | "postcss": "^8.4.14",
29 | "tailwindcss": "^3.1.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css'
2 | import Header from '../components/Header'
3 | import Footer from '../components/Footer'
4 |
5 | import {AuthProvider} from '../context/AuthContext'
6 |
7 |
8 | function MyApp({ Component, pageProps }) {
9 |
10 | return (
11 |
12 |
17 |
18 | )
19 | }
20 | export default MyApp
21 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/account/login.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useRouter } from "next/router";
3 | import useAuth from "../../hooks/useAuth";
4 |
5 | const Login = () => {
6 | const [email, setEmail] = useState("");
7 | const [password, setPassword] = useState("");
8 | const [error, setError] = useState(null);
9 |
10 | const { setUser } = useAuth();
11 |
12 | const router = useRouter();
13 | const handleSubmit = async (e) => {
14 | e.preventDefault();
15 | // call the API route
16 |
17 | const res = await fetch("/api/login", {
18 | method: "POST",
19 | headers: { "Content-Type": "application/json" },
20 | body: JSON.stringify({ email, password }),
21 | });
22 | if (res.ok) {
23 | const user = await res.json();
24 | setUser(user);
25 | router.push("/");
26 | } else {
27 | const errData = await res.json();
28 | setError(errData);
29 | }
30 | };
31 |
32 | return (
33 |
34 |
Login
35 | {error && (
36 |
37 | {error.detail}
38 |
39 | )}
40 |
41 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default Login;
77 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/account/logout.jsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { useEffect } from "react";
3 | import useAuth from "../../hooks/useAuth";
4 |
5 | const Logout = () => {
6 | const { user, setUser } = useAuth();
7 | const removeCookie = async () => {
8 | const res = await fetch("/api/logout", {
9 | method: "POST",
10 | headers: { "Content-Type": "application/json" },
11 | });
12 | };
13 | const router = useRouter();
14 | useEffect(() => {
15 | removeCookie();
16 | setUser(null);
17 |
18 | router.push("/");
19 | }, []);
20 |
21 | return <>>;
22 | };
23 |
24 | export default Logout;
25 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/account/register.jsx:
--------------------------------------------------------------------------------
1 | const Register = () => {
2 | return register
;
3 | };
4 |
5 | export default Register;
6 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/api/login.js:
--------------------------------------------------------------------------------
1 | import cookie from 'cookie'
2 |
3 | export default async (req, res)=>{
4 | if (req.method==='POST'){
5 |
6 | const {email, password} = req.body
7 |
8 | const result = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/login`, {
9 | method:'POST',
10 | headers:{'Content-Type':'application/json'},
11 | body:JSON.stringify({email, password})
12 | })
13 |
14 | const data = await result.json()
15 | if (result.ok){
16 | const jwt = data.token
17 | res.status(200).setHeader('Set-Cookie', cookie.serialize(
18 | 'jwt',jwt,
19 | {
20 | path:'/',
21 | httpOnly: true,
22 | sameSite:'strict',
23 | maxAge:1800
24 |
25 | }
26 | )).json({
27 | 'username':data['user']['username'],
28 | 'email':data['user']['email'],
29 | 'role':data['user']['role'],
30 | 'jwt':jwt
31 | })
32 | } else {
33 |
34 | data['error'] = data['detail']
35 | res.status(401)
36 | res.json(data)
37 | return
38 | }
39 |
40 |
41 | } else {
42 | res.setHeader('Allow',['POST'])
43 | res.status(405).json({message:`Method ${req.method} not allowed`})
44 | return
45 | }
46 | }
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/api/logout.js:
--------------------------------------------------------------------------------
1 | import cookie from 'cookie'
2 |
3 |
4 | export default async (req, res)=>{
5 |
6 | res.status(200).setHeader('Set-Cookie', cookie.serialize(
7 | 'jwt','',
8 | {
9 | path:'/',
10 | httpOnly: true,
11 | sameSite:'strict',
12 | maxAge:-1
13 | }
14 | )
15 |
16 | )
17 | res.json({"message":"success"})
18 | res.end()
19 | }
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/api/user.js:
--------------------------------------------------------------------------------
1 |
2 | export default async function(req, res){
3 | if (req.method==='GET'){
4 | const {jwt} = req.cookies;
5 |
6 | if(!jwt){
7 | res.status(401).end()
8 | return;
9 | }
10 |
11 | try {
12 | const result = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/me`, {
13 | method:'GET',
14 | headers:{
15 | 'Content-Type':'application/json',
16 | 'Authorization':`Bearer ${jwt}`
17 |
18 | }
19 | })
20 | const userData = await result.json()
21 | userData['jwt'] = jwt
22 | res.status(200).json(userData).end()
23 | } catch (error) {
24 | res.status(401).end()
25 | return;
26 | }
27 |
28 |
29 | } else {
30 | res.setHeader('Allow',['GET'])
31 | res.status(405).json({message:`Method ${req.method} not allowed`}).end()
32 | }
33 | }
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/cars/[id].jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | export const getStaticPaths = async () => {
3 | const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cars`);
4 | const cars = await res.json();
5 |
6 | const paths = cars.map((car) => ({
7 | params: { id: car._id },
8 | }));
9 |
10 | return { paths, fallback: "blocking" };
11 | };
12 |
13 | export const getStaticProps = async ({ params: { id } }) => {
14 | const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cars/${id}`);
15 | const car = await res.json();
16 |
17 | return {
18 | props: { car },
19 | revalidate: 10,
20 | };
21 | };
22 |
23 | const CarById = ({ car }) => {
24 | return (
25 |
26 |
27 | {car.brand} - {car.make}
28 |
29 |
30 |
31 |
32 |
{`This fine car was manufactured in ${car.year}, it made just ${car.km} km and it sports a ${car.cm3} cm3 engine.`}
33 |
34 |
Price: {car.price} eur
35 |
36 | );
37 | };
38 |
39 | export default CarById;
40 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/cars/index.js:
--------------------------------------------------------------------------------
1 | import Card from "../../components/Card"
2 |
3 | export const getServerSideProps = async () => {
4 |
5 |
6 |
7 | const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cars/`);
8 | const cars = await res.json();
9 |
10 | return {
11 | props: {
12 | cars,
13 | revalidate: 10,
14 | },
15 | };
16 | };
17 |
18 | const Cars = ({ cars }) => {
19 |
20 | return (
21 |
22 |
Available Cars
23 |
24 |
25 | {cars.map((car) => {
26 | const {_id, brand, make, picture, year, km, cm3, price} = car
27 | return (
28 |
29 |
40 |
41 | );
42 | })}
43 |
44 |
45 | );
46 | };
47 |
48 | export default Cars;
49 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/pages/index.js:
--------------------------------------------------------------------------------
1 | const HomePage = () => {
2 |
3 |
4 | return (
5 |
6 |
7 | FARM Cars
8 |
9 |
10 | )
11 | }
12 | export default HomePage
13 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter8/frontend/next-cars/public/favicon.ico
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/chapter8/frontend/next-cars/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./pages/**/*.{js,ts,jsx,tsx}",
4 | "./components/**/*.{js,ts,jsx,tsx}",
5 | ],
6 | theme: {
7 | extend: {
8 |
9 | },
10 | },
11 | plugins: [
12 | require('@tailwindcss/forms')
13 | ],
14 | }
15 |
--------------------------------------------------------------------------------
/chapter9/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105 | __pypackages__/
106 |
107 | # Celery stuff
108 | celerybeat-schedule
109 | celerybeat.pid
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | testvenv/
120 | ENV/
121 | env.bak/
122 | venv.bak/
123 |
124 | # Spyder project settings
125 | .spyderproject
126 | .spyproject
127 |
128 | # Rope project settings
129 | .ropeproject
130 |
131 | # mkdocs documentation
132 | /site
133 |
134 | # mypy
135 | .mypy_cache/
136 | .dmypy.json
137 | dmypy.json
138 |
139 | # Pyre type checker
140 | .pyre/
141 |
142 | # pytype static type analyzer
143 | .pytype/
144 |
145 | # Cython debug symbols
146 | cython_debug/
147 |
148 | # PyCharm
149 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
150 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
151 | # and can be added to the global gitignore or merged into this file. For a more nuclear
152 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
153 | #.idea/
154 | .vercel
155 |
--------------------------------------------------------------------------------
/chapter9/backend/cars.json:
--------------------------------------------------------------------------------
1 | [{
2 | "_id": {
3 | "model": "Marea"
4 | },
5 | "avgKm": 226773.5
6 | },{
7 | "_id": {
8 | "model": "Ulysse"
9 | },
10 | "avgKm": 226565
11 | },{
12 | "_id": {
13 | "model": "Stilo"
14 | },
15 | "avgKm": 207230
16 | },{
17 | "_id": {
18 | "model": "Tempra"
19 | },
20 | "avgKm": 203500
21 | },{
22 | "_id": {
23 | "model": "Multipla"
24 | },
25 | "avgKm": 194302.14285714287
26 | },{
27 | "_id": {
28 | "model": "Croma"
29 | },
30 | "avgKm": 192818.45833333334
31 | },{
32 | "_id": {
33 | "model": "Palio"
34 | },
35 | "avgKm": 188650
36 | },{
37 | "_id": {
38 | "model": "Punto"
39 | },
40 | "avgKm": 179875.5
41 | },{
42 | "_id": {
43 | "model": "Idea"
44 | },
45 | "avgKm": 174078.58333333334
46 | },{
47 | "_id": {
48 | "model": "Sedici"
49 | },
50 | "avgKm": 171750.2
51 | },{
52 | "_id": {
53 | "model": "Grande Punto"
54 | },
55 | "avgKm": 167311.23376623375
56 | },{
57 | "_id": {
58 | "model": "Seicento"
59 | },
60 | "avgKm": 165000
61 | },{
62 | "_id": {
63 | "model": "Bravo"
64 | },
65 | "avgKm": 161005.92
66 | },{
67 | "_id": {
68 | "model": "Doblo"
69 | },
70 | "avgKm": 158813.56603773584
71 | },{
72 | "_id": {
73 | "model": "500L"
74 | },
75 | "avgKm": 154006
76 | },{
77 | "_id": {
78 | "model": "Ducato"
79 | },
80 | "avgKm": 152316.75
81 | },{
82 | "_id": {
83 | "model": "Panda"
84 | },
85 | "avgKm": 147225.375
86 | },{
87 | "_id": {
88 | "model": "Freemonte"
89 | },
90 | "avgKm": 145444.14285714287
91 | },{
92 | "_id": {
93 | "model": "Fiorino"
94 | },
95 | "avgKm": 139750
96 | },{
97 | "_id": {
98 | "model": "Qubo"
99 | },
100 | "avgKm": 134404
101 | },{
102 | "_id": {
103 | "model": "Scudo"
104 | },
105 | "avgKm": 129957.66666666667
106 | },{
107 | "_id": {
108 | "model": "500"
109 | },
110 | "avgKm": 125122.86363636363
111 | },{
112 | "_id": {
113 | "model": "Tipo"
114 | },
115 | "avgKm": 91632.66666666667
116 | },{
117 | "_id": {
118 | "model": "500X"
119 | },
120 | "avgKm": 83000
121 | },{
122 | "_id": {
123 | "model": "500C"
124 | },
125 | "avgKm": 55000
126 | }]
--------------------------------------------------------------------------------
/chapter9/backend/importScript.py:
--------------------------------------------------------------------------------
1 | import csv
2 | from fastapi.encoders import jsonable_encoder
3 | from pymongo import MongoClient
4 | from decouple import config
5 | from models import CarBase
6 |
7 | # connect to mongodb
8 | DB_URL = config("DB_URL", cast=str)
9 | DB_NAME = config("DB_NAME", cast=str)
10 |
11 | client = MongoClient(DB_URL)
12 | db = client[DB_NAME]
13 | cars = db["cars"]
14 |
15 |
16 | #
17 |
18 | # read csv
19 | with open("filteredCars.csv", encoding="utf-8") as f:
20 | csv_reader = csv.DictReader(f)
21 | name_records = list(csv_reader)
22 |
23 |
24 | for rec in name_records:
25 | print(rec)
26 |
27 | try:
28 | rec["cm3"] = int(rec["cm3"])
29 | rec["price"] = int(rec["price"])
30 | rec["year"] = int(rec["year"])
31 | rec["km"] = int(rec["km"])
32 | rec["brand"] = str(rec["brand"])
33 | rec["make"] = str(rec["make"])
34 |
35 | car = jsonable_encoder(CarBase(**rec))
36 | print("Inserting:", car)
37 | cars.insert_one(car)
38 |
39 | except ValueError:
40 | pass
41 |
--------------------------------------------------------------------------------
/chapter9/backend/main.py:
--------------------------------------------------------------------------------
1 | from decouple import config
2 | from fastapi import FastAPI
3 |
4 | import aioredis
5 | from fastapi_cache import FastAPICache
6 | from fastapi_cache.backends.redis import RedisBackend
7 |
8 | # there seems to be a bug with FastAPI's middleware
9 | # https://stackoverflow.com/questions/65191061/fastapi-cors-middleware-not-working-with-get-method/65994876#65994876
10 |
11 | from starlette.middleware import Middleware
12 | from starlette.middleware.cors import CORSMiddleware
13 |
14 |
15 | middleware = [
16 | Middleware(
17 | CORSMiddleware,
18 | allow_origins=["*"],
19 | allow_credentials=True,
20 | allow_methods=["*"],
21 | allow_headers=["*"],
22 | )
23 | ]
24 |
25 | from motor.motor_asyncio import AsyncIOMotorClient
26 |
27 | from routers.cars import router as cars_router
28 |
29 |
30 | DB_URL = config("DB_URL", cast=str)
31 | DB_NAME = config("DB_NAME", cast=str)
32 |
33 |
34 | # define origins
35 | origins = [
36 | "*",
37 | ]
38 |
39 | # instantiate the app
40 | app = FastAPI(middleware=middleware)
41 |
42 | app.include_router(cars_router, prefix="/cars", tags=["cars"])
43 |
44 |
45 | @app.on_event("startup")
46 | async def startup_db_client():
47 | app.mongodb_client = AsyncIOMotorClient(DB_URL)
48 | app.mongodb = app.mongodb_client[DB_NAME]
49 | redis = aioredis.from_url(
50 | "redis://localhost:6379", encoding="utf8", decode_responses=True
51 | )
52 | FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
53 |
54 |
55 | @app.on_event("shutdown")
56 | async def shutdown_db_client():
57 | app.mongodb_client.close()
58 |
--------------------------------------------------------------------------------
/chapter9/backend/models.py:
--------------------------------------------------------------------------------
1 | from bson import ObjectId
2 |
3 | from pydantic import Field, BaseModel, validator
4 |
5 |
6 | class PyObjectId(ObjectId):
7 | @classmethod
8 | def __get_validators__(cls):
9 | yield cls.validate
10 |
11 | @classmethod
12 | def validate(cls, v):
13 | if not ObjectId.is_valid(v):
14 | raise ValueError("Invalid objectid")
15 | return ObjectId(v)
16 |
17 | @classmethod
18 | def __modify_schema__(cls, field_schema):
19 | field_schema.update(type="string")
20 |
21 |
22 | class MongoBaseModel(BaseModel):
23 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
24 |
25 | class Config:
26 | json_encoders = {ObjectId: str}
27 |
28 |
29 | class CarBase(MongoBaseModel):
30 |
31 | brand: str = Field(..., min_length=2)
32 | make: str = Field(..., min_length=1)
33 | year: int = Field(..., gt=1975, lt=2023)
34 | price: int = Field(...)
35 | km: int = Field(...)
36 | cm3: int = Field(..., gt=400, lt=8000)
37 |
--------------------------------------------------------------------------------
/chapter9/backend/random_forest_pipe.joblib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/backend/random_forest_pipe.joblib
--------------------------------------------------------------------------------
/chapter9/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | anyio==3.6.1
2 | click==8.1.3
3 | colorama==0.4.5
4 | dnslib==0.9.20
5 | dnspython==2.2.1
6 | fastapi==0.79.0
7 | h11==0.13.0
8 | idna==3.3
9 | motor==3.0.0
10 | numpy==1.23.1
11 | pandas==1.4.3
12 | pydantic==1.9.1
13 | pymongo==4.2.0
14 | python-dateutil==2.8.2
15 | python-decouple==3.6
16 | python-http-client==3.3.7
17 | PythonDNS==0.1
18 | pytz==2022.1
19 | sendgrid==6.9.7
20 | six==1.16.0
21 | sniffio==1.2.0
22 | starkbank-ecdsa==2.0.3
23 | starlette==0.19.1
24 | typing_extensions==4.3.0
25 | uvicorn==0.18.2
26 | joblib
27 |
--------------------------------------------------------------------------------
/chapter9/backend/routers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/backend/routers/__init__.py
--------------------------------------------------------------------------------
/chapter9/backend/routers/cars.py:
--------------------------------------------------------------------------------
1 | from json import load
2 | from math import ceil
3 |
4 | from typing import List, Optional
5 |
6 | from fastapi import APIRouter, Request, Body, HTTPException, BackgroundTasks
7 |
8 |
9 | from fastapi_cache.decorator import cache
10 |
11 | from models import CarBase
12 |
13 | from utils.report import report_pipeline
14 |
15 | import joblib
16 | import pandas as pd
17 |
18 |
19 | router = APIRouter()
20 |
21 |
22 | @router.get("/all", response_description="List all cars")
23 | async def list_all_cars(
24 | request: Request,
25 | min_price: int = 0,
26 | max_price: int = 100000,
27 | brand: Optional[str] = None,
28 | page: int = 1,
29 | ) -> List[CarBase]:
30 |
31 | RESULTS_PER_PAGE = 25
32 | skip = (page - 1) * RESULTS_PER_PAGE
33 |
34 | query = {"price": {"$lt": max_price, "$gt": min_price}}
35 | if brand:
36 | query["brand"] = brand
37 |
38 | # count total docs
39 | pages = ceil(
40 | await request.app.mongodb["cars"].count_documents(query) / RESULTS_PER_PAGE
41 | )
42 |
43 | full_query = (
44 | request.app.mongodb["cars"]
45 | .find(query)
46 | .sort("km", -1)
47 | .skip(skip)
48 | .limit(RESULTS_PER_PAGE)
49 | )
50 |
51 | results = [CarBase(**raw_car) async for raw_car in full_query]
52 |
53 | return {"results": results, "pages": pages}
54 |
55 |
56 | # sample of N cars
57 | @router.get("/sample/{n}", response_description="Sample of N cars")
58 | @cache(expire=60)
59 | async def get_sample(n: int, request: Request):
60 |
61 | query = [
62 | {"$match": {"year": {"$gt": 2010}}},
63 | {
64 | "$project": {
65 | "_id": 0,
66 | }
67 | },
68 | {"$sample": {"size": n}},
69 | {"$sort": {"brand": 1, "make": 1, "year": 1}},
70 | ]
71 |
72 | full_query = request.app.mongodb["cars"].aggregate(query)
73 | results = [el async for el in full_query]
74 | return results
75 |
76 |
77 | # aggregation by model / avg price
78 | @router.get("/brand/{val}/{brand}", response_description="Get brand models by val")
79 | async def brand_price(brand: str, val: str, request: Request):
80 |
81 | query = [
82 | {"$match": {"brand": brand}},
83 | {"$project": {"_id": 0}},
84 | {
85 | "$group": {"_id": {"model": "$make"}, f"avg_{val}": {"$avg": f"${val}"}},
86 | },
87 | {"$sort": {f"avg_{val}": 1}},
88 | ]
89 |
90 | full_query = request.app.mongodb["cars"].aggregate(query)
91 | return [el async for el in full_query]
92 |
93 |
94 | # count cars by brand
95 | @router.get("/brand/count", response_description="Count by brand")
96 | async def brand_count(request: Request):
97 |
98 | query = [{"$group": {"_id": "$brand", "count": {"$sum": 1}}}]
99 |
100 | full_query = request.app.mongodb["cars"].aggregate(query)
101 | return [el async for el in full_query]
102 |
103 |
104 | # count cars by make
105 | @router.get("/make/count/{brand}", response_description="Count by brand")
106 | async def brand_count(brand: str, request: Request):
107 |
108 | query = [
109 | {"$match": {"brand": brand}},
110 | {"$group": {"_id": "$make", "count": {"$sum": 1}}},
111 | {"$sort": {"count": -1}},
112 | ]
113 |
114 | full_query = request.app.mongodb["cars"].aggregate(query)
115 | results = [el async for el in full_query]
116 | return results
117 |
118 |
119 | @router.post("/email", response_description="Send email")
120 | async def send_mail(
121 | background_tasks: BackgroundTasks,
122 | cars_num: int = Body(...),
123 | email: str = Body(...),
124 | ):
125 |
126 | background_tasks.add_task(report_pipeline, email, cars_num)
127 |
128 | return {"Received": {"email": email, "cars_num": cars_num}}
129 |
130 |
131 | @router.post("/predict", response_description="Predict price")
132 | async def predict(
133 | brand: str = Body(...),
134 | make: str = Body(...),
135 | year: int = Body(...),
136 | cm3: int = Body(...),
137 | km: int = Body(...),
138 | ):
139 |
140 | print(brand, make, year, cm3, km)
141 | loaded_model = joblib.load("./random_forest_pipe.joblib")
142 |
143 | # = Body?
144 | input_data = {
145 | "brand": brand,
146 | "make": make,
147 | "year": year,
148 | "cm3": cm3,
149 | "km": km,
150 | }
151 |
152 | from_db_df = pd.DataFrame(input_data, index=[0])
153 |
154 | prediction = float(loaded_model.predict(from_db_df)[0])
155 | return {"prediction": prediction}
156 |
--------------------------------------------------------------------------------
/chapter9/backend/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/backend/utils/__init__.py
--------------------------------------------------------------------------------
/chapter9/backend/utils/report.py:
--------------------------------------------------------------------------------
1 | from .report_query import make_query
2 | from .send_report import send_report
3 |
4 |
5 | def report_pipeline(email, cars_number):
6 |
7 | # make the query - get the data and some HTML
8 | try:
9 | query_data = make_query(cars_number)
10 | except Exception as e:
11 | print(e)
12 | print("Couldn't make the query")
13 |
14 | try:
15 | send_report(email=email, subject="FARM Cars Report", HTMLcontent=query_data)
16 |
17 | except Exception as e:
18 | print(e)
19 |
--------------------------------------------------------------------------------
/chapter9/backend/utils/report_query.py:
--------------------------------------------------------------------------------
1 | # a function that returns a sample of N cars
2 | from pymongo import MongoClient
3 | from decouple import config
4 |
5 | import pandas as pd
6 |
7 | # connect to mongodb
8 | DB_URL = config("DB_URL", cast=str)
9 | DB_NAME = config("DB_NAME", cast=str)
10 |
11 | client = MongoClient(DB_URL)
12 | db = client[DB_NAME]
13 | cars = db["cars"]
14 |
15 |
16 | def make_query(cars_number: int):
17 |
18 | query = [
19 | {"$match": {"year": {"$gt": 2010}}},
20 | {
21 | "$project": {
22 | "_id": 0,
23 | }
24 | },
25 | {"$sample": {"size": cars_number}},
26 | {"$sort": {"brand": 1, "make": 1, "year": 1}},
27 | ]
28 |
29 | full_query = cars.aggregate(query)
30 | results = [el for el in full_query]
31 |
32 | HTML = pd.DataFrame(results).to_html(index=False)
33 |
34 | return HTML
35 |
--------------------------------------------------------------------------------
/chapter9/backend/utils/send_report.py:
--------------------------------------------------------------------------------
1 | # send report by email
2 | from decouple import config
3 |
4 | import sendgrid
5 | from sendgrid.helpers.mail import *
6 |
7 | SENDGRID_ID = config("SENDGRID_ID", cast=str)
8 |
9 |
10 | def send_report(email, subject, HTMLcontent):
11 |
12 | sg = sendgrid.SendGridAPIClient(api_key=SENDGRID_ID)
13 | from_email = Email("FARM@freethrow.rs")
14 | to_email = To(email)
15 | subject = "FARM Cars daily report"
16 | content = Content(
17 | "text/plain", "this is dynamic text, potentially coming from our database"
18 | )
19 |
20 | mail = Mail(from_email, to_email, subject, content, html_content=HTMLcontent)
21 |
22 | try:
23 | response = sg.client.mail.send.post(request_body=mail.get())
24 | print(response)
25 | print("Sending email")
26 | print(response.status_code)
27 | print(response.body)
28 | print(response.headers)
29 |
30 | except Exception as e:
31 | print(e)
32 | print("Could not send email")
33 |
--------------------------------------------------------------------------------
/chapter9/frontend/.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 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/chapter9/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/chapter9/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.4",
7 | "@testing-library/react": "^13.3.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "chart.js": "^3.8.0",
10 | "d3-scale-chromatic": "^3.0.0",
11 | "daisyui": "^2.20.0",
12 | "react": "^18.2.0",
13 | "react-chartjs-2": "^4.3.1",
14 | "react-dom": "^18.2.0",
15 | "react-pdf-tailwind": "^1.0.1",
16 | "react-router-dom": "^6.3.0",
17 | "react-scripts": "5.0.1",
18 | "react-select": "^5.4.0",
19 | "swr": "^1.3.0",
20 | "web-vitals": "^2.1.4"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "devDependencies": {
47 | "@tailwindcss/forms": "^0.5.2",
48 | "autoprefixer": "^10.4.7",
49 | "postcss": "^8.4.14",
50 | "tailwindcss": "^3.1.6"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/chapter9/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/chapter9/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/chapter9/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | FARM Car Analytics
28 |
29 |
30 |
31 | You need to enable JavaScript to run this app.
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/chapter9/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/frontend/public/logo192.png
--------------------------------------------------------------------------------
/chapter9/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/frontend/public/logo512.png
--------------------------------------------------------------------------------
/chapter9/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/chapter9/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/App.js:
--------------------------------------------------------------------------------
1 |
2 | function App() {
3 | return (
4 |
7 | );
8 | }
9 |
10 | export default App;
11 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/BrandCount.jsx:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 |
3 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
4 | import { Pie } from "react-chartjs-2";
5 |
6 | import { schemePastel1 } from "d3-scale-chromatic";
7 |
8 | const colors = schemePastel1;
9 |
10 | ChartJS.register(ArcElement, Tooltip, Legend);
11 |
12 | const fetcher = (...args) => fetch(...args).then((res) => res.json());
13 |
14 | const BrandCount = () => {
15 | const { data, error } = useSWR(
16 | `${process.env.REACT_APP_API_URL}/cars/brand/count`,
17 | fetcher
18 | );
19 | if (error) return failed to load
;
20 | if (!data) return loading...
;
21 |
22 | const chartData = {
23 | labels: data.map((item) => {
24 | return item["_id"];
25 | }),
26 | datasets: [
27 | {
28 | data: data.map((item) => item.count),
29 | backgroundColor: data.map((item, index) => colors[index]),
30 | borderWidth: 3,
31 | },
32 | ],
33 | };
34 |
35 | return (
36 |
37 |
38 | Vehicle Count by Brand
39 |
40 |
41 |
44 |
45 | );
46 | };
47 |
48 | export default BrandCount;
49 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/BrandValue.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import useSWR from "swr";
3 |
4 | import {
5 | Chart as ChartJS,
6 | CategoryScale,
7 | LinearScale,
8 | BarElement,
9 | Title,
10 | Tooltip,
11 | Legend,
12 | } from "chart.js";
13 | import { Bar } from "react-chartjs-2";
14 | import CarsDropdown from "./CarsDropdown";
15 |
16 | ChartJS.register(
17 | CategoryScale,
18 | LinearScale,
19 | BarElement,
20 | Title,
21 | Tooltip,
22 | Legend
23 | );
24 |
25 | export const options = (val) => {
26 | let optObj = {
27 | responsive: true,
28 | plugins: {
29 | legend: {
30 | position: "top",
31 | },
32 | title: {
33 | display: true,
34 | text: `Average ${val} of car models by brand`,
35 | },
36 | },
37 | };
38 | if (val === "year") {
39 | optObj["scales"] = {
40 | y: {
41 | min: 1980,
42 | },
43 | };
44 | }
45 | return optObj;
46 | };
47 |
48 | const fetcher = (...args) => fetch(...args).then((res) => res.json());
49 |
50 | const BrandValue = ({ val }) => {
51 | const queryStr = `avg_${val}`;
52 |
53 | const [brand, setBrand] = useState("Fiat");
54 |
55 | const { data, error } = useSWR(
56 | `${process.env.REACT_APP_API_URL}/cars/brand/${val}/${brand}`,
57 | fetcher
58 | );
59 |
60 | if (error) return failed to load
;
61 | if (!data) return loading...
;
62 |
63 | const chartData = {
64 | labels: data.map((item) => {
65 | return item["_id"]["model"];
66 | }),
67 | datasets: [
68 | {
69 | label: brand,
70 | data: data.map((item) => Math.round(item[queryStr])),
71 | hoverBackgroundColor: ["#B91C1C"],
72 | },
73 | ],
74 | };
75 |
76 | return (
77 |
78 |
79 | {val.toUpperCase()} by model for a given brand - {brand}
80 |
81 |
82 |
83 | setBrand(event.target.value)}
85 | elValue={brand}
86 | />
87 |
88 |
89 |
90 |
91 |
92 | );
93 | };
94 |
95 | export default BrandValue;
96 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/Card.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Card = ({ car }) => {
4 | const { make, brand, km, cm3, price, year } = car;
5 |
6 | return (
7 |
8 |
9 | {brand} - {make} ({cm3}cm3)
10 |
11 |
12 | {km} Km / Year: {year}
13 |
14 |
Price: {price} eur
15 |
16 | );
17 | };
18 |
19 | export default Card;
20 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/CarsDropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const CarsDropdown = ({ selectHandler, allCars, elValue }) => {
4 | const carBrands = [
5 | "Fiat",
6 | "Opel",
7 | "Renault",
8 | "Peugeot",
9 | "VW",
10 | "Ford",
11 | "Honda",
12 | "Toyota",
13 | ];
14 | return (
15 |
20 | {allCars && All brands }
21 | {carBrands.map((brand) => {
22 | return (
23 |
24 | {brand}
25 |
26 | );
27 | })}
28 |
29 | );
30 | };
31 |
32 | CarsDropdown.defaultProps = {
33 | allCars: false,
34 | elValue: "",
35 | };
36 | export default CarsDropdown;
37 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import BrandCount from "./BrandCount";
2 | import ModelCount from "./ModelCount";
3 | import BrandValue from "./BrandValue";
4 |
5 | const Dashboard = () => {
6 | return (
7 |
8 |
9 | DashBoard
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default Dashboard;
24 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Footer = () => {
4 | return (
5 |
6 | Footer
7 |
8 | );
9 | };
10 |
11 | export default Footer;
12 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from "react-router-dom";
2 | const Header = () => {
3 | return (
4 |
5 |
6 | (isActive ? "active menu" : "menu")}
8 | to="/"
9 | >
10 | Cars
11 |
12 | (isActive ? "active menu" : "menu")}
14 | to="/dashboard"
15 | >
16 | Dashboard
17 |
18 | (isActive ? "active menu" : "menu")}
20 | to="/report"
21 | >
22 | Report
23 |
24 |
25 |
26 | );
27 | };
28 | export default Header;
29 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/Home.jsx:
--------------------------------------------------------------------------------
1 | // fetcher for SWR
2 | import { useState } from "react";
3 | import useSWR from "swr";
4 | import Card from "./Card";
5 | import CarsDropdown from "./CarsDropdown";
6 |
7 | const fetcher = (...args) => fetch(...args).then((res) => res.json());
8 |
9 | const Home = () => {
10 | const [pageIndex, setPageIndex] = useState(1);
11 | const [brand, setBrand] = useState("");
12 |
13 | const { nextData, nextError } = useSWR(
14 | `${process.env.REACT_APP_API_URL}/cars/all?page=${
15 | pageIndex + 1
16 | }&brand=${brand}`,
17 | fetcher
18 | );
19 |
20 | const { data, error } = useSWR(
21 | `${process.env.REACT_APP_API_URL}/cars/all?page=${pageIndex}&brand=${brand}`,
22 | fetcher
23 | );
24 |
25 | if (error) return `failed to load {process.env.REACT_APP_API_URL}`
;
26 | if (!data) return loading...
;
27 |
28 | return (
29 |
30 |
31 | Explore Cars
32 |
33 |
34 | {JSON.stringify(nextData)} {JSON.stringify(nextError)}
35 |
36 |
37 |
38 |
{
40 | setBrand(event.target.value);
41 | setPageIndex(1);
42 | }}
43 | allCars={true}
44 | elValue={brand}
45 | />
46 |
47 | {pageIndex > 1 ? (
48 | setPageIndex(pageIndex - 1)}
51 | >
52 | Previous
53 |
54 | ) : (
55 | <>>
56 | )}
57 | {pageIndex < data.pages ? (
58 | setPageIndex(pageIndex + 1)}
61 | >
62 | Next
63 |
64 | ) : (
65 | <>>
66 | )}
67 |
68 |
69 | Brand:
70 |
71 | {brand ? brand : "All brands"}
72 |
73 | Page:
74 |
75 | {pageIndex} of {data.pages}
76 |
77 |
78 |
79 |
80 |
81 | {data.results.map((car) => (
82 |
83 | ))}
84 |
85 |
86 | );
87 | };
88 |
89 | export default Home;
90 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import Header from "./Header";
2 | import Footer from "./Footer";
3 |
4 | const Layout = ({ children }) => {
5 | return (
6 |
7 |
8 |
9 | {children}
10 |
11 |
12 |
13 | );
14 | };
15 | export default Layout;
16 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/ModelCount.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import useSWR from "swr";
3 |
4 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
5 | import { Pie } from "react-chartjs-2";
6 |
7 | import { interpolateGnBu } from "d3-scale-chromatic";
8 | import CarsDropdown from "./CarsDropdown";
9 |
10 | ChartJS.register(ArcElement, Tooltip, Legend);
11 |
12 | const fetcher = (...args) => fetch(...args).then((res) => res.json());
13 |
14 | const ModelCount = () => {
15 | const [brand, setBrand] = useState("Fiat");
16 | const { data, error } = useSWR(
17 | `${process.env.REACT_APP_API_URL}/cars/make/count/${brand}`,
18 | fetcher
19 | );
20 | if (error) return failed to load
;
21 | if (!data) return loading...
;
22 |
23 | const chartData = {
24 | labels: data.map((item) => {
25 | return item["_id"];
26 | }),
27 | datasets: [
28 | {
29 | data: data.map((item) => item.count),
30 | backgroundColor: data
31 | .map((item, index) => interpolateGnBu(index / data.length))
32 | .sort(() => 0.5 - Math.random()),
33 | borderWidth: 3,
34 | },
35 | ],
36 | };
37 |
38 | console.log(chartData);
39 |
40 | return (
41 |
42 |
43 | Number of vehicles by model for a given brand
44 |
45 |
46 | setBrand(event.target.value)}
48 | elValue={brand}
49 | />
50 |
51 |
52 |
55 |
56 | );
57 | };
58 |
59 | export default ModelCount;
60 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/components/Report.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | const Report = () => {
4 | const [email, setEmail] = useState("");
5 | const [carsNum, setCarsNum] = useState(10);
6 | const [message, setMessage] = useState("");
7 | const [loading, setLoading] = useState(false);
8 |
9 | const handleForm = async (e) => {
10 | e.preventDefault();
11 | setLoading(true);
12 | const res = await fetch(`${process.env.REACT_APP_API_URL}/cars/email`, {
13 | method: "POST",
14 | headers: { "Content-Type": "application/json" },
15 | body: JSON.stringify({ email, cars_num: carsNum }),
16 | });
17 |
18 | if (res.ok) {
19 | setLoading(false);
20 | setMessage(`Report with ${carsNum} cars sent to ${email}!`);
21 | }
22 | };
23 |
24 | return (
25 |
26 |
27 | Generate Report
28 |
29 |
30 | {loading && (
31 |
32 | Generating and sending report in the background...
33 |
34 | )}
35 |
36 | {message && (
37 |
38 | {message}
39 |
40 | )}
41 |
42 | {!loading && !message && (
43 |
71 | )}
72 |
73 |
74 | );
75 | };
76 |
77 | export default Report;
78 |
--------------------------------------------------------------------------------
/chapter9/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | @layer components {
7 | .menu {
8 | @apply p-2 my-2 rounded-lg
9 | }
10 | .active {
11 | @apply text-red-800 border border-red-800
12 | }
13 | }
--------------------------------------------------------------------------------
/chapter9/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import {BrowserRouter,Routes,Route,
4 | } from "react-router-dom";
5 |
6 | import './index.css';
7 | import Layout from './components/Layout';
8 | import Home from './components/Home';
9 | import Dashboard from './components/Dashboard';
10 | import Report from './components/Report';
11 |
12 |
13 | const root = ReactDOM.createRoot(document.getElementById('root'));
14 | root.render(
15 |
16 |
17 |
18 |
19 | } />
20 | } />
21 | } />
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 |
--------------------------------------------------------------------------------
/chapter9/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./src/**/*.{js,jsx,ts,tsx}",
4 | ],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [
9 | require('@tailwindcss/forms'),
10 | require("daisyui")
11 | ],
12 | }
13 |
--------------------------------------------------------------------------------