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
- User registration and authentication with JWT tokens
- Secure password hashing with bcrypt
- User-specific todo items
- Create, read, update, and delete todo items
- User profile management
- Role-based access control
- Health endpoint for application monitoring
- API documentation via Swagger UI and ReDoc
- Database migrations using Alembic
@ -15,6 +20,7 @@ This is a REST API for a simple todo application built with FastAPI and SQLite.
```
├── app/
│ ├── api/ # API endpoints
│ ├── core/ # Core functionality, security, dependencies
│ ├── crud/ # Database CRUD operations
│ ├── db/ # Database connection and utilities
│ ├── models/ # SQLAlchemy models
@ -51,14 +57,41 @@ The API will be available at http://localhost:8000
## API Endpoints
- `GET /`: Root endpoint with API information
- `GET /health`: Health check endpoint
- `GET /todos`: Get all todo items
### Authentication
- `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
- `GET /todos/{id}`: Get a specific todo item
- `PATCH /todos/{id}`: Update 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
Run migrations with:

View File

@ -1,3 +1,5 @@
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 sqlalchemy.orm import Session
from app.core.deps import get_current_active_user
from app.crud import todo as todo_crud
from app.db.database import get_db
from app.models.user import User
from app.schemas.todo import Todo, TodoCreate, TodoUpdate
router = APIRouter(
@ -15,50 +17,72 @@ router = APIRouter(
@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])
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
@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:
raise HTTPException(status_code=404, detail="Todo not found")
return db_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:
raise HTTPException(status_code=404, detail="Todo not found")
return db_todo
@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:
raise HTTPException(status_code=404, detail="Todo not found")
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.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()
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]:
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(
title=todo.title,
description=todo.description,
completed=todo.completed,
owner_id=owner_id,
)
db.add(db_todo)
db.commit()
@ -26,8 +35,8 @@ def create_todo(db: Session, todo: TodoCreate) -> Todo:
return db_todo
def update_todo(db: Session, todo_id: int, todo: TodoUpdate) -> Optional[Todo]:
db_todo = get_todo(db, todo_id)
def update_todo(db: Session, todo_id: int, todo: TodoUpdate, owner_id: int) -> Optional[Todo]:
db_todo = get_todo_by_owner(db, todo_id, owner_id)
if db_todo is None:
return None
@ -40,8 +49,8 @@ def update_todo(db: Session, todo_id: int, todo: TodoUpdate) -> Optional[Todo]:
return db_todo
def delete_todo(db: Session, todo_id: int) -> bool:
db_todo = get_todo(db, todo_id)
def delete_todo(db: Session, todo_id: int, owner_id: int) -> bool:
db_todo = get_todo_by_owner(db, todo_id, owner_id)
if db_todo is None:
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.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.orm import relationship
from app.db.database import Base
@ -12,4 +13,8 @@ class Todo(Base):
description = Column(String)
completed = Column(Boolean, default=False)
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.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):
id: int
owner_id: int
created_at: datetime
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.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
# Create tables in database
Base.metadata.create_all(bind=engine)
app = FastAPI(
title="Simple Todo Application",
description="A simple todo application API built with FastAPI",
version="0.1.0",
title="Simple Todo Application with Authentication",
description="A todo application API built with FastAPI and user authentication",
version="0.2.0",
)
# Setup CORS
@ -23,6 +23,8 @@ app.add_middleware(
)
# Include routers
app.include_router(auth_router)
app.include_router(users_router)
app.include_router(todos_router)
# Health endpoint
@ -40,7 +42,7 @@ def read_root():
Root endpoint with links to API documentation
"""
return {
"message": "Welcome to the Simple Todo Application API",
"message": "Welcome to the Simple Todo Application API with User Authentication",
"documentation": "/docs",
"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

@ -4,4 +4,7 @@ sqlalchemy==2.0.22
pydantic==2.4.2
alembic==1.12.0
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