ZhangZhihui's Blog  

 

(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ pip install passlib
Collecting passlib
  Downloading passlib-1.7.4-py2.py3-none-any.whl.metadata (1.7 kB)
Downloading passlib-1.7.4-py2.py3-none-any.whl (525 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 525.6/525.6 kB 1.7 MB/s  0:00:00
Installing collected packages: passlib
Successfully installed passlib-1.7.4
(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ pip install bcrypt
Collecting bcrypt
  Downloading bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl.metadata (10 kB)
Downloading bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl (278 kB)
Installing collected packages: bcrypt
Successfully installed bcrypt-5.0.0

 

Got below error when creating users:

ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])

 

Uninstall bcrypt and install an older version:

(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ pip uninstall bcrypt
Found existing installation: bcrypt 5.0.0
Uninstalling bcrypt-5.0.0:
  Would remove:
    /home/frank/fastapi-venv/lib/python3.13/site-packages/bcrypt-5.0.0.dist-info/*
    /home/frank/fastapi-venv/lib/python3.13/site-packages/bcrypt/*
Proceed (Y/n)? Y
  Successfully uninstalled bcrypt-5.0.0
(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ pip install bcrypt==4.0.1
Collecting bcrypt==4.0.1
  Downloading bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl.metadata (9.0 kB)
Downloading bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl (593 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 593.7/593.7 kB 568.9 kB/s  0:00:01
Installing collected packages: bcrypt
Successfully installed bcrypt-4.0.1

 

(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ uvicorn main:app --reload
INFO:     Will watch for changes in these directories: ['/home/frank/zproject/TodoApp']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [103674] using StatReload
Form data requires "python-multipart" to be installed. 
You can install "python-multipart" with: 

pip install python-multipart

(Many errors occurred)

 

(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ pip install python-multipart
Collecting python-multipart
  Downloading python_multipart-0.0.22-py3-none-any.whl.metadata (1.8 kB)
Downloading python_multipart-0.0.22-py3-none-any.whl (24 kB)
Installing collected packages: python-multipart
Successfully installed python-multipart-0.0.22
(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ uvicorn main:app --reload
INFO:     Will watch for changes in these directories: ['/home/frank/zproject/TodoApp']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [104553] using StatReload
INFO:     Started server process [104561]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

(No error)

 

1

 

2

 

3

 

4

 

5

 

6

 

7

 

(fastapi-venv) frank@ZZHPC:~/zproject/TodoApp$ pip install "python-jose[cryptography]"
Collecting python-jose[cryptography]
  Downloading python_jose-3.5.0-py2.py3-none-any.whl.metadata (5.5 kB)
Collecting ecdsa!=0.15 (from python-jose[cryptography])
  Downloading ecdsa-0.19.1-py2.py3-none-any.whl.metadata (29 kB)
Collecting rsa!=4.1.1,!=4.4,<5.0,>=4.0 (from python-jose[cryptography])
  Downloading rsa-4.9.1-py3-none-any.whl.metadata (5.6 kB)
Collecting pyasn1>=0.5.0 (from python-jose[cryptography])
  Downloading pyasn1-0.6.2-py3-none-any.whl.metadata (8.4 kB)
Collecting cryptography>=3.4.0 (from python-jose[cryptography])
  Downloading cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl.metadata (5.7 kB)
Collecting cffi>=2.0.0 (from cryptography>=3.4.0->python-jose[cryptography])
  Downloading cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.6 kB)
Collecting pycparser (from cffi>=2.0.0->cryptography>=3.4.0->python-jose[cryptography])
  Downloading pycparser-3.0-py3-none-any.whl.metadata (8.2 kB)
Collecting six>=1.9.0 (from ecdsa!=0.15->python-jose[cryptography])
  Downloading six-1.17.0-py2.py3-none-any.whl.metadata (1.7 kB)
Downloading python_jose-3.5.0-py2.py3-none-any.whl (34 kB)
Downloading rsa-4.9.1-py3-none-any.whl (34 kB)
Downloading cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl (4.5 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 3.3 MB/s  0:00:01
Downloading cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (219 kB)
Downloading ecdsa-0.19.1-py2.py3-none-any.whl (150 kB)
Downloading pyasn1-0.6.2-py3-none-any.whl (83 kB)
Downloading six-1.17.0-py2.py3-none-any.whl (11 kB)
Downloading pycparser-3.0-py3-none-any.whl (48 kB)
Installing collected packages: six, pycparser, pyasn1, rsa, ecdsa, cffi, python-jose, cryptography
Successfully installed cffi-2.0.0 cryptography-46.0.5 ecdsa-0.19.1 pyasn1-0.6.2 pycparser-3.0 python-jose-3.5.0 rsa-4.9.1 six-1.17.0

 

1

 

2

 

3

 

main.py:

from fastapi import FastAPI


import models
from database import engine
from routers import auth, todos, admin, user


app = FastAPI()

models.Base.metadata.create_all(bind=engine)

app.include_router(auth.router)
app.include_router(todos.router)
app.include_router(admin.router)
app.include_router(user.router)

 

database.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.ext.declarative import declarative_base


DATABASE_URL = 'sqlite:///./todosapp.db'

engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
# Well, we want to say check same thread of type false.
# And now by default, SQLite will only allow one thread to communicate with it. Assuming that each thread will handle an independent request.
# This is to prevent any kind of accident sharing of the same connection for different kind of requests.
# But in fast API it's very normal to have more than one thread that could interact with the database at the same time.
# So we just need to make sure SQLite knows that hey, we don't want to be checking the same thread all the time because there could be multiple threads happening to our SQLite database.

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Now we need to create a SessionLocal and each instance of the SessionLocal will have a database session.
# The class itself is not a database session yet. We will add that later on.
# But right now we just need to be able to create an instance of SessionLocal that will be able to become an actual database in the future.

Base = declarative_base()
# create an object of our database which will then be able to interact with the tables that we create in the future.


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

 

models.py:

from database import Base
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey


class Users(Base):
    __tablename__ = 'users'

    # To make a column auto-increment, you typically need to define it as a Primary Key with an integer type.
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True)
    username = Column(String, unique=True)
    first_name = Column(String)
    last_name = Column(String)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)
    role = Column(String)


class Todos(Base):
    __tablename__ = 'todos'

    # To make a column auto-increment, you typically need to define it as a Primary Key with an integer type.
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String)
    description = Column(String)
    description = Column(String)
    priority = Column(Integer)
    complete = Column(Boolean, default=False)
    owner_id = Column(Integer, ForeignKey("users.id"))

 

routers/auth.py:

from typing import Annotated
from datetime import timedelta, datetime, timezone

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from starlette import status
from passlib.context import CryptContext
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from jose import jwt, JWTError

from database import Session, get_db
from models import Users


router = APIRouter(
    prefix='/auth',
    tags=['auth']
)

SECRET_KEY = '197b2c37c391bed93fe80344fe73b806947a65e36206e05a1a23c2fa12702fe3'
ALGORITHM = 'HS256'

bcrypt_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
oauth2_bearer = OAuth2PasswordBearer(tokenUrl='auth/token')

# Dependency injection
db_dependency = Annotated[Session, Depends(get_db)]
token_dependency = Annotated[str, Depends(oauth2_bearer)]


class CreateUserRequest(BaseModel):
    username: str
    email: str
    first_name: str
    last_name: str
    password: str
    role: str


class Token(BaseModel):
    access_token: str
    token_type: str


def authenticate_user(username: str, password: str, db):
    user = db.query(Users).filter(Users.username == username).first()
    if not user:
        return False
    if not bcrypt_context.verify(password, user.hashed_password):
        return False
    return user


def create_access_token(username: str, user_id: int, role: str, expires_delta: timedelta):
    encode = {'sub': username, 'id': user_id, 'role': role}
    expires = datetime.now(timezone.utc) + expires_delta
    encode.update({'exp': expires})
    return jwt.encode(encode, SECRET_KEY, algorithm=ALGORITHM)



async def get_current_user(token: token_dependency):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get('sub')
        user_id: int = payload.get('id')
        user_role: str = payload.get('role')
        if username is None or user_id is None:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Could not validate user.')
        return {'username': username, 'id': user_id, 'user_role': user_role}
    except JWTError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Could not validate user.')


@router.post("/", status_code=status.HTTP_201_CREATED)
async def create_user(db: db_dependency,
                      create_user_request: CreateUserRequest):
    create_user_model = Users(
        email=create_user_request.email,
        username=create_user_request.username,
        first_name=create_user_request.first_name,
        last_name=create_user_request.last_name,
        role=create_user_request.role,
        hashed_password=bcrypt_context.hash(create_user_request.password),
        is_active=True
    )

    db.add(create_user_model)
    db.commit()


@router.post("/token", response_model=Token)
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
                                 db: db_dependency):
    user = authenticate_user(form_data.username, form_data.password, db)
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Could not validate user.')
    token = create_access_token(user.username, user.id, user.role, timedelta(minutes=20))

    return {'access_token': token, 'token_type': 'bearer'}

 

routers/user.py:

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from starlette import status

from database import Session, get_db
from models import Users
from .auth import get_current_user, bcrypt_context


router = APIRouter(
    prefix='/user',
    tags=['user']
)

# Dependency injection
db_dependency = Annotated[Session, Depends(get_db)]
user_dependency = Annotated[dict, Depends(get_current_user)]


class UserVerification(BaseModel):
    password: str
    new_password: str = Field(min_length=6)


@router.get('/', status_code=status.HTTP_200_OK)
async def get_user(user: user_dependency, db: db_dependency):
    if user is None:
        raise HTTPException(status_code=401, detail='Authentication Failed')
    return db.query(Users).filter(Users.id == user.get('id')).first()


@router.put('/password', status_code=status.HTTP_204_NO_CONTENT)
async def change_password(user: user_dependency, db: db_dependency, user_verification: UserVerification):
    if user is None:
        raise HTTPException(status_code=401, detail='Authentication Failed')
    user_model = db.query(Users).filter(Users.id == user.get('id')).first()

    if not bcrypt_context.verify(user_verification.password, user_model.hashed_password):
        raise HTTPException(status_code=401, detail='Authentication Failed')

    user_model.hashed_password = bcrypt_context.hash(user_verification.new_password)
    db.add(user_model)
    db.commit()

 

routers/admin.py:

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, Path
from starlette import status

from database import Session, get_db
from models import Todos
from .auth import get_current_user


router = APIRouter(
    prefix='/admin',
    tags=['admin']
)

# Dependency injection
db_dependency = Annotated[Session, Depends(get_db)]
user_dependency = Annotated[dict, Depends(get_current_user)]


@router.get("/todo", status_code=status.HTTP_200_OK)
async def read_all(user: user_dependency, db: db_dependency):
    if user is None or user.get('user_role') != 'admin':
        raise HTTPException(status_code=401, detail='Authentication Failed')
    return db.query(Todos).all()


@router.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(user: user_dependency, db: db_dependency, todo_id: int = Path(gt=0)):
    if user is None or user.get('user_role') != 'admin':
        raise HTTPException(status_code=401, detail='Authentication Failed')

    todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
    if todo_model is None:
        raise HTTPException(status_code=404, detail='Todo not found.')
    db.query(Todos).filter(Todos.id == todo_id).delete()
    db.commit()

 

routers/todos.py:

from typing import Annotated

from fastapi import APIRouter, Depends
from fastapi import HTTPException, Path
from pydantic import BaseModel, Field
from starlette import status

from database import Session, get_db
from models import Todos
from .auth import get_current_user


router = APIRouter()

# Dependency injection
db_dependency = Annotated[Session, Depends(get_db)]
user_dependency = Annotated[dict, Depends(get_current_user)]


class TodoRequest(BaseModel):
    title: str = Field(min_length=3)
    description: str = Field(min_length=3, max_length=100)
    priority: int = Field(gt=0, lt=6)
    complete: bool


@router.get("/", status_code=status.HTTP_200_OK)
async def read_all(user: user_dependency, db: db_dependency):
    if user is None:
        raise HTTPException(status_code=401, detail='Authentication Failed')
    return db.query(Todos).filter(Todos.owner_id == user.get('id')).all()


@router.get("/todo/{todo_id}", status_code=status.HTTP_200_OK)
async def read_todo(user: user_dependency, db: db_dependency, todo_id: int = Path(gt=0)):
    if user is None:
        raise HTTPException(status_code=401, detail='Authentication Failed')

    todo_model = db.query(Todos).filter(Todos.id == todo_id)\
        .filter(Todos.owner_id == user.get('id')).first()
    if todo_model is not None:
        return todo_model
    raise HTTPException(status_code=404, detail='Todo not found.')


@router.post("/todo", status_code=status.HTTP_201_CREATED)
async def create_todo(user: user_dependency, db: db_dependency, todo_request: TodoRequest):
    if user is None:
        raise HTTPException(status_code=401, detail='Authentication Failed')
    todo_model = Todos(**todo_request.model_dump(), owner_id=user.get('id'))

    db.add(todo_model)
    db.commit()


@router.put("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def update_todo(user: user_dependency,
                      db: db_dependency,
                      todo_request: TodoRequest,  # TodoRequest must be above any Path parameter
                      todo_id: int = Path(gt=0)):
    if user is None:
        raise HTTPException(status_code=401, detail='Authentication Failed')

    todo_model = db.query(Todos).filter(Todos.id == todo_id)\
        .filter(Todos.owner_id == user.get('id')).first()
    if todo_model is None:
        raise HTTPException(status_code=404, detail='Todo not found.')
    
    todo_model.title = todo_request.title
    todo_model.description = todo_request.description
    todo_model.priority = todo_request.priority
    todo_model.complete = todo_request.complete

    db.add(todo_model)
    db.commit()


@router.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(user: user_dependency, db: db_dependency, todo_id: int = Path(gt=0)):
    if user is None:
        raise HTTPException(status_code=401, detail='Authentication Failed')

    todo_model = db.query(Todos).filter(Todos.id == todo_id)\
        .filter(Todos.owner_id == user.get('id')).first()
    if todo_model is None:
        raise HTTPException(status_code=404, detail='Todo not found.')
    db.query(Todos).filter(Todos.id == todo_id).filter(Todos.owner_id == user.get('id')).delete()

    db.commit()

 

posted on 2026-03-07 17:02  ZhangZhihuiAAA  阅读(26)  评论(0)    收藏  举报