Add user authentication to Todo application

- Create User model and schema
- Implement password hashing with bcrypt
- Add JWT token-based authentication
- Create user and auth endpoints
- Update todo endpoints with user authentication
- Add alembic migration for user model
- Update README with new features
This commit is contained in:
Automated Action 2025-05-16 02:07:51 +00:00
parent b399d38a23
commit 69f6a404bd
20 changed files with 678 additions and 37 deletions

View File

@ -1,10 +1,15 @@
# Simple Todo Application API # Simple Todo Application API with Authentication
This is a REST API for a simple todo application built with FastAPI and SQLite. This is a REST API for a todo application built with FastAPI and SQLite, featuring user authentication and authorization.
## Features ## Features
- User registration and authentication with JWT tokens
- Secure password hashing with bcrypt
- User-specific todo items
- Create, read, update, and delete todo items - Create, read, update, and delete todo items
- User profile management
- Role-based access control
- Health endpoint for application monitoring - Health endpoint for application monitoring
- API documentation via Swagger UI and ReDoc - API documentation via Swagger UI and ReDoc
- Database migrations using Alembic - Database migrations using Alembic
@ -15,6 +20,7 @@ This is a REST API for a simple todo application built with FastAPI and SQLite.
``` ```
├── app/ ├── app/
│ ├── api/ # API endpoints │ ├── api/ # API endpoints
│ ├── core/ # Core functionality, security, dependencies
│ ├── crud/ # Database CRUD operations │ ├── crud/ # Database CRUD operations
│ ├── db/ # Database connection and utilities │ ├── db/ # Database connection and utilities
│ ├── models/ # SQLAlchemy models │ ├── models/ # SQLAlchemy models
@ -51,14 +57,41 @@ The API will be available at http://localhost:8000
## API Endpoints ## API Endpoints
- `GET /`: Root endpoint with API information ### Authentication
- `GET /health`: Health check endpoint
- `GET /todos`: Get all todo items - `POST /auth/register`: Register a new user
- `POST /auth/login`: Login and get access token
- `POST /auth/refresh`: Refresh access token
- `GET /auth/me`: Get current user information
### Users
- `GET /users/`: Get all users (requires authentication)
- `GET /users/{id}`: Get a specific user by ID (requires authentication)
- `PATCH /users/{id}`: Update a user (requires authentication and ownership)
- `DELETE /users/{id}`: Delete a user (requires authentication and ownership)
### Todo Items
- `GET /todos`: Get all todo items for the current user
- `POST /todos`: Create a new todo item - `POST /todos`: Create a new todo item
- `GET /todos/{id}`: Get a specific todo item - `GET /todos/{id}`: Get a specific todo item
- `PATCH /todos/{id}`: Update a todo item - `PATCH /todos/{id}`: Update a todo item
- `DELETE /todos/{id}`: Delete a todo item - `DELETE /todos/{id}`: Delete a todo item
**Note:** All todo operations require authentication and only access to the user's own todos is allowed.
### Other
- `GET /`: Root endpoint with API information
- `GET /health`: Health check endpoint
## Authentication Flow
1. Register a new user: `POST /auth/register`
2. Login to get a JWT token: `POST /auth/login`
3. Use the token in the Authorization header for all subsequent requests: `Authorization: Bearer {token}`
## Database Migrations ## Database Migrations
Run migrations with: Run migrations with:

View File

@ -1,3 +1,5 @@
from app.api.todos import router as todos_router from app.api.todos import router as todos_router
from app.api.auth import router as auth_router
from app.api.users import router as users_router
__all__ = ["todos_router"] __all__ = ["todos_router", "auth_router", "users_router"]

94
app/api/auth.py Normal file
View File

@ -0,0 +1,94 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.deps import get_current_user
from app.core.security import (
ACCESS_TOKEN_EXPIRE_MINUTES,
create_access_token,
)
from app.crud.user import authenticate_user, create_user, get_user_by_email, get_user_by_username
from app.db.database import get_db
from app.models.user import User
from app.schemas.user import Token, User as UserSchema, UserCreate
router = APIRouter(
prefix="/auth",
tags=["authentication"],
)
@router.post("/login", response_model=Token)
def login_for_access_token(
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
subject=user.id, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/refresh", response_model=Token)
def refresh_token(
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
) -> Any:
"""
Refresh access token
"""
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
subject=current_user.id, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/register", response_model=UserSchema, status_code=status.HTTP_201_CREATED)
def register_user(*, db: Session = Depends(get_db), user_in: UserCreate) -> Any:
"""
Register a new user
"""
# Check if user with this email already exists
user = get_user_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A user with this email already exists",
)
# Check if user with this username already exists
user = get_user_by_username(db, username=user_in.username)
if user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A user with this username already exists",
)
# Create new user
user = create_user(db, user_in)
return user
@router.get("/me", response_model=UserSchema)
def read_users_me(current_user: User = Depends(get_current_user)) -> Any:
"""
Get current user information
"""
return current_user

View File

@ -3,8 +3,10 @@ from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user
from app.crud import todo as todo_crud from app.crud import todo as todo_crud
from app.db.database import get_db from app.db.database import get_db
from app.models.user import User
from app.schemas.todo import Todo, TodoCreate, TodoUpdate from app.schemas.todo import Todo, TodoCreate, TodoUpdate
router = APIRouter( router = APIRouter(
@ -15,50 +17,72 @@ router = APIRouter(
@router.post("/", response_model=Todo, status_code=status.HTTP_201_CREATED) @router.post("/", response_model=Todo, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)): def create_todo(
todo: TodoCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
""" """
Create a new todo item. Create a new todo item for the current user.
""" """
return todo_crud.create_todo(db=db, todo=todo) return todo_crud.create_todo(db=db, todo=todo, owner_id=current_user.id)
@router.get("/", response_model=List[Todo]) @router.get("/", response_model=List[Todo])
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): def read_todos(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
""" """
Retrieve all todo items with pagination. Retrieve all todo items for the current user with pagination.
""" """
todos = todo_crud.get_todos(db, skip=skip, limit=limit) todos = todo_crud.get_todos_by_owner(db, owner_id=current_user.id, skip=skip, limit=limit)
return todos return todos
@router.get("/{todo_id}", response_model=Todo) @router.get("/{todo_id}", response_model=Todo)
def read_todo(todo_id: int, db: Session = Depends(get_db)): def read_todo(
todo_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
""" """
Retrieve a specific todo item by ID. Retrieve a specific todo item by ID for the current user.
""" """
db_todo = todo_crud.get_todo(db, todo_id=todo_id) db_todo = todo_crud.get_todo_by_owner(db, todo_id=todo_id, owner_id=current_user.id)
if db_todo is None: if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")
return db_todo return db_todo
@router.patch("/{todo_id}", response_model=Todo) @router.patch("/{todo_id}", response_model=Todo)
def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)): def update_todo(
todo_id: int,
todo: TodoUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
""" """
Update a todo item. Update a todo item for the current user.
""" """
db_todo = todo_crud.update_todo(db, todo_id=todo_id, todo=todo) db_todo = todo_crud.update_todo(db, todo_id=todo_id, todo=todo, owner_id=current_user.id)
if db_todo is None: if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")
return db_todo return db_todo
@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(todo_id: int, db: Session = Depends(get_db)): def delete_todo(
todo_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
""" """
Delete a todo item. Delete a todo item for the current user.
""" """
success = todo_crud.delete_todo(db, todo_id=todo_id) success = todo_crud.delete_todo(db, todo_id=todo_id, owner_id=current_user.id)
if not success: if not success:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")
return None return None

91
app/api/users.py Normal file
View File

@ -0,0 +1,91 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user
from app.crud.user import get_user, get_users, update_user, delete_user
from app.db.database import get_db
from app.models.user import User
from app.schemas.user import User as UserSchema, UserUpdate
router = APIRouter(
prefix="/users",
tags=["users"],
dependencies=[Depends(get_current_active_user)],
responses={404: {"description": "User not found"}},
)
@router.get("/", response_model=List[UserSchema])
def read_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve users.
"""
users = get_users(db, skip=skip, limit=limit)
return users
@router.get("/{user_id}", response_model=UserSchema)
def read_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get a specific user by id.
"""
user = get_user(db, user_id=user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
@router.patch("/{user_id}", response_model=UserSchema)
def update_user_endpoint(
user_id: int,
user_in: UserUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a user.
"""
# Only allow users to update their own information
if current_user.id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions to update this user"
)
user = update_user(db, user_id=user_id, user=user_in)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user_endpoint(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a user.
"""
# Only allow users to delete their own account
if current_user.id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions to delete this user"
)
success = delete_user(db, user_id=user_id)
if not success:
raise HTTPException(status_code=404, detail="User not found")
return None

1
app/core/__init__.py Normal file
View File

@ -0,0 +1 @@
# Init file for core package

85
app/core/deps.py Normal file
View File

@ -0,0 +1,85 @@
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from app.core.security import ALGORITHM, SECRET_KEY
from app.crud.user import get_user
from app.db.database import get_db
from app.models.user import User
from app.schemas.user import TokenData
# OAuth2 configuration
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
"""
Validates the access token and returns the current user
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
token_data = TokenData(user_id=int(user_id))
except JWTError:
raise credentials_exception
user = get_user(db, user_id=token_data.user_id)
if user is None:
raise credentials_exception
return user
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
"""
Checks if the current user is active
"""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user"
)
return current_user
def get_optional_current_user(
db: Session = Depends(get_db), token: Optional[str] = Depends(oauth2_scheme)
) -> Optional[User]:
"""
Similar to get_current_user but doesn't raise an exception for missing token
"""
if token is None:
return None
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
return None
token_data = TokenData(user_id=int(user_id))
except JWTError:
return None
user = get_user(db, user_id=token_data.user_id)
if user is None or not user.is_active:
return None
return user

53
app/core/security.py Normal file
View File

@ -0,0 +1,53 @@
from datetime import datetime, timedelta
from typing import Any, Optional, Union
from jose import jwt
from passlib.context import CryptContext
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Secret key and algorithm for JWT
SECRET_KEY = "a_very_secret_key_that_should_be_in_env_vars" # In production, use env vars
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify if the provided password matches the stored hashed password
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""
Hash a password for storing
"""
return pwd_context.hash(password)
def create_access_token(
subject: Union[str, Any], expires_delta: Optional[timedelta] = None
) -> str:
"""
Create a JWT access token
"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {"exp": expire, "sub": str(subject)}
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def create_refresh_token(subject: Union[str, Any]) -> str:
"""
Create a JWT refresh token with longer expiry
"""
expire = datetime.utcnow() + timedelta(days=7) # Refresh tokens are valid for 7 days
to_encode = {"exp": expire, "sub": str(subject), "type": "refresh"}
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

View File

@ -1,3 +1,11 @@
from app.crud.todo import create_todo, delete_todo, get_todo, get_todos, update_todo from app.crud.todo import create_todo, delete_todo, get_todo, get_todos, update_todo
from app.crud.user import (
authenticate_user, create_user, delete_user, get_user, get_user_by_email,
get_user_by_username, get_users, update_user
)
__all__ = ["create_todo", "delete_todo", "get_todo", "get_todos", "update_todo"] __all__ = [
"create_todo", "delete_todo", "get_todo", "get_todos", "update_todo",
"authenticate_user", "create_user", "delete_user", "get_user", "get_user_by_email",
"get_user_by_username", "get_users", "update_user"
]

View File

@ -10,15 +10,24 @@ def get_todo(db: Session, todo_id: int) -> Optional[Todo]:
return db.query(Todo).filter(Todo.id == todo_id).first() return db.query(Todo).filter(Todo.id == todo_id).first()
def get_todo_by_owner(db: Session, todo_id: int, owner_id: int) -> Optional[Todo]:
return db.query(Todo).filter(Todo.id == todo_id, Todo.owner_id == owner_id).first()
def get_todos(db: Session, skip: int = 0, limit: int = 100) -> List[Todo]: def get_todos(db: Session, skip: int = 0, limit: int = 100) -> List[Todo]:
return db.query(Todo).offset(skip).limit(limit).all() return db.query(Todo).offset(skip).limit(limit).all()
def create_todo(db: Session, todo: TodoCreate) -> Todo: def get_todos_by_owner(db: Session, owner_id: int, skip: int = 0, limit: int = 100) -> List[Todo]:
return db.query(Todo).filter(Todo.owner_id == owner_id).offset(skip).limit(limit).all()
def create_todo(db: Session, todo: TodoCreate, owner_id: int) -> Todo:
db_todo = Todo( db_todo = Todo(
title=todo.title, title=todo.title,
description=todo.description, description=todo.description,
completed=todo.completed, completed=todo.completed,
owner_id=owner_id,
) )
db.add(db_todo) db.add(db_todo)
db.commit() db.commit()
@ -26,8 +35,8 @@ def create_todo(db: Session, todo: TodoCreate) -> Todo:
return db_todo return db_todo
def update_todo(db: Session, todo_id: int, todo: TodoUpdate) -> Optional[Todo]: def update_todo(db: Session, todo_id: int, todo: TodoUpdate, owner_id: int) -> Optional[Todo]:
db_todo = get_todo(db, todo_id) db_todo = get_todo_by_owner(db, todo_id, owner_id)
if db_todo is None: if db_todo is None:
return None return None
@ -40,8 +49,8 @@ def update_todo(db: Session, todo_id: int, todo: TodoUpdate) -> Optional[Todo]:
return db_todo return db_todo
def delete_todo(db: Session, todo_id: int) -> bool: def delete_todo(db: Session, todo_id: int, owner_id: int) -> bool:
db_todo = get_todo(db, todo_id) db_todo = get_todo_by_owner(db, todo_id, owner_id)
if db_todo is None: if db_todo is None:
return False return False

101
app/crud/user.py Normal file
View File

@ -0,0 +1,101 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
def get_user(db: Session, user_id: int) -> Optional[User]:
"""
Get a user by ID
"""
return db.query(User).filter(User.id == user_id).first()
def get_user_by_email(db: Session, email: str) -> Optional[User]:
"""
Get a user by email
"""
return db.query(User).filter(User.email == email).first()
def get_user_by_username(db: Session, username: str) -> Optional[User]:
"""
Get a user by username
"""
return db.query(User).filter(User.username == username).first()
def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[User]:
"""
Get a list of users with pagination
"""
return db.query(User).offset(skip).limit(limit).all()
def create_user(db: Session, user: UserCreate) -> User:
"""
Create a new user
"""
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
username=user.username,
hashed_password=hashed_password,
is_active=user.is_active,
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def update_user(
db: Session, user_id: int, user: UserUpdate
) -> Optional[User]:
"""
Update user information
"""
db_user = get_user(db, user_id)
if not db_user:
return None
update_data = user.model_dump(exclude_unset=True)
# Hash the password if it's being updated
if "password" in update_data:
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
for key, value in update_data.items():
setattr(db_user, key, value)
db.commit()
db.refresh(db_user)
return db_user
def delete_user(db: Session, user_id: int) -> bool:
"""
Delete a user
"""
db_user = get_user(db, user_id)
if not db_user:
return False
db.delete(db_user)
db.commit()
return True
def authenticate_user(db: Session, username: str, password: str) -> Optional[User]:
"""
Authenticate a user by username and password
"""
user = get_user_by_username(db, username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user

View File

@ -1,3 +1,4 @@
from app.models.todo import Todo from app.models.todo import Todo
from app.models.user import User
__all__ = ["Todo"] __all__ = ["Todo", "User"]

View File

@ -1,5 +1,6 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime from sqlalchemy import Boolean, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.database import Base from app.db.database import Base
@ -13,3 +14,7 @@ class Todo(Base):
completed = Column(Boolean, default=False) completed = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# New fields for user association
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="todos")

20
app/models/user.py Normal file
View File

@ -0,0 +1,20 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationship with Todo items
todos = relationship("Todo", back_populates="owner", cascade="all, delete-orphan")

View File

@ -1,3 +1,7 @@
from app.schemas.todo import Todo, TodoBase, TodoCreate, TodoUpdate from app.schemas.todo import Todo, TodoBase, TodoCreate, TodoUpdate
from app.schemas.user import User, UserBase, UserCreate, UserUpdate, UserInDB, Token, TokenData
__all__ = ["Todo", "TodoBase", "TodoCreate", "TodoUpdate"] __all__ = [
"Todo", "TodoBase", "TodoCreate", "TodoUpdate",
"User", "UserBase", "UserCreate", "UserUpdate", "UserInDB", "Token", "TokenData"
]

View File

@ -22,6 +22,7 @@ class TodoUpdate(BaseModel):
class Todo(TodoBase): class Todo(TodoBase):
id: int id: int
owner_id: int
created_at: datetime created_at: datetime
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None

46
app/schemas/user.py Normal file
View File

@ -0,0 +1,46 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class UserBase(BaseModel):
email: EmailStr
username: str
is_active: Optional[bool] = True
class UserCreate(UserBase):
password: str = Field(..., min_length=6)
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
password: Optional[str] = Field(None, min_length=6)
is_active: Optional[bool] = None
class User(UserBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
from_attributes = True
class UserInDB(User):
hashed_password: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
email: Optional[str] = None
user_id: Optional[int] = None

12
main.py
View File

@ -1,16 +1,16 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.api import todos_router from app.api import auth_router, todos_router, users_router
from app.db.database import Base, engine from app.db.database import Base, engine
# Create tables in database # Create tables in database
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
app = FastAPI( app = FastAPI(
title="Simple Todo Application", title="Simple Todo Application with Authentication",
description="A simple todo application API built with FastAPI", description="A todo application API built with FastAPI and user authentication",
version="0.1.0", version="0.2.0",
) )
# Setup CORS # Setup CORS
@ -23,6 +23,8 @@ app.add_middleware(
) )
# Include routers # Include routers
app.include_router(auth_router)
app.include_router(users_router)
app.include_router(todos_router) app.include_router(todos_router)
# Health endpoint # Health endpoint
@ -40,7 +42,7 @@ def read_root():
Root endpoint with links to API documentation Root endpoint with links to API documentation
""" """
return { return {
"message": "Welcome to the Simple Todo Application API", "message": "Welcome to the Simple Todo Application API with User Authentication",
"documentation": "/docs", "documentation": "/docs",
"alternative_doc": "/redoc", "alternative_doc": "/redoc",
} }

View File

@ -0,0 +1,58 @@
"""User authentication and Todo-User relationship
Revision ID: 2
Depends on: 1
Create Date: 2023-10-28
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2'
down_revision = '1'
branch_labels = None
depends_on = None
def upgrade():
# Create users table
op.create_table(
'users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)')),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# Create indexes on users table
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
# Add owner_id column to todos table
op.add_column('todos', sa.Column('owner_id', sa.Integer(), nullable=True))
# Create foreign key constraint
op.create_foreign_key('fk_todos_owner_id_users', 'todos', 'users', ['owner_id'], ['id'])
def downgrade():
# Drop foreign key constraint
op.drop_constraint('fk_todos_owner_id_users', 'todos', type_='foreignkey')
# Drop owner_id column from todos table
op.drop_column('todos', 'owner_id')
# Drop indexes on users table
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
# Drop users table
op.drop_table('users')

View File

@ -5,3 +5,6 @@ pydantic==2.4.2
alembic==1.12.0 alembic==1.12.0
python-multipart==0.0.6 python-multipart==0.0.6
ruff==0.1.1 ruff==0.1.1
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
pydantic-settings==2.0.3