Add complete simple messaging app with FastAPI

- Implement user authentication with JWT tokens
- Add messaging system for sending/receiving messages
- Create SQLite database with SQLAlchemy models
- Set up Alembic for database migrations
- Add health check endpoint
- Include comprehensive API documentation
- Support user registration, login, and message management
- Enable conversation history and user listing
This commit is contained in:
Automated Action 2025-06-26 16:07:21 +00:00
parent 7b85c77220
commit ca5dbb9088
23 changed files with 636 additions and 2 deletions

View File

@ -1,3 +1,72 @@
# FastAPI Application
# Simple Messaging App
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A simple messaging application built with FastAPI, SQLAlchemy, and SQLite.
## Features
- User registration and authentication with JWT tokens
- Send and receive messages between users
- View conversation history
- Health check endpoint
- API documentation with Swagger UI
## Tech Stack
- **FastAPI**: Modern, fast web framework for building APIs
- **SQLAlchemy**: SQL toolkit and ORM
- **SQLite**: Lightweight database
- **Alembic**: Database migration tool
- **JWT**: JSON Web Tokens for authentication
- **Bcrypt**: Password hashing
- **Ruff**: Fast Python linter
## Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Run database migrations:
```bash
alembic upgrade head
```
3. Start the application:
```bash
uvicorn main:app --reload
```
The application will be available at `http://localhost:8000`
## API Documentation
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`
- OpenAPI JSON: `http://localhost:8000/openapi.json`
## Endpoints
### Authentication
- `POST /auth/register` - Register a new user
- `POST /auth/login` - Login user and get access token
### Messages
- `POST /messages/send` - Send a message to another user
- `GET /messages/received` - Get received messages
- `GET /messages/sent` - Get sent messages
- `GET /messages/conversation/{user_id}` - Get conversation with specific user
- `GET /messages/users` - Get list of all users
### Health
- `GET /health` - Health check endpoint
## Environment Variables
Set the following environment variables:
- `SECRET_KEY`: JWT secret key for token signing (default: "your-secret-key-here")
## Database
The application uses SQLite database stored at `/app/storage/db/db.sqlite`. The database schema is managed with Alembic migrations.

42
alembic.ini Normal file
View File

@ -0,0 +1,42 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
output_encoding = utf-8
[post_write_hooks]
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

54
alembic/env.py Normal file
View File

@ -0,0 +1,54 @@
from logging.config import fileConfig
import sys
import os
from pathlib import Path
sys.path.append(str(Path(__file__).resolve().parent.parent))
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from app.db.base import Base
from app.models.user import User
from app.models.message import Message
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
alembic/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,54 @@
"""Initial migration
Revision ID: 001
Revises:
Create Date: 2024-01-01 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# 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),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
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)
# Create messages table
op.create_table('messages',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('sender_id', sa.Integer(), nullable=False),
sa.Column('receiver_id', sa.Integer(), nullable=False),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.ForeignKeyConstraint(['receiver_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['sender_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False)
def downgrade() -> None:
op.drop_index(op.f('ix_messages_id'), table_name='messages')
op.drop_table('messages')
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')
op.drop_table('users')

0
app/__init__.py Normal file
View File

0
app/auth/__init__.py Normal file
View File

71
app/auth/auth.py Normal file
View File

@ -0,0 +1,71 @@
import os
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.user import User
from app.schemas.user import TokenData
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user_by_email(db: Session, email: str):
return db.query(User).filter(User.email == email).first()
def authenticate_user(db: Session, email: str, password: str):
user = get_user_by_email(db, email)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
token = credentials.credentials
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = TokenData(email=email)
except JWTError:
raise credentials_exception
user = get_user_by_email(db, email=token_data.email)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user

0
app/db/__init__.py Normal file
View File

3
app/db/base.py Normal file
View File

@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

26
app/db/session.py Normal file
View File

@ -0,0 +1,26 @@
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db.base import Base
DB_DIR = Path("/app/storage/db")
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
Base.metadata.create_all(bind=engine)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

0
app/models/__init__.py Normal file
View File

16
app/models/message.py Normal file
View File

@ -0,0 +1,16 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.base import Base
class Message(Base):
__tablename__ = "messages"
id = Column(Integer, primary_key=True, index=True)
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False)
receiver_id = Column(Integer, ForeignKey("users.id"), nullable=False)
content = Column(Text, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
sender = relationship("User", foreign_keys=[sender_id])
receiver = relationship("User", foreign_keys=[receiver_id])

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

@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy.sql import func
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
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())

0
app/routers/__init__.py Normal file
View File

58
app/routers/auth.py Normal file
View File

@ -0,0 +1,58 @@
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserLogin, Token, User as UserSchema
from app.auth.auth import (
authenticate_user,
create_access_token,
get_password_hash,
get_user_by_email,
ACCESS_TOKEN_EXPIRE_MINUTES
)
router = APIRouter()
@router.post("/register", response_model=UserSchema)
def register_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(
status_code=400,
detail="Email already registered"
)
db_user = db.query(User).filter(User.username == user.username).first()
if db_user:
raise HTTPException(
status_code=400,
detail="Username already taken"
)
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
username=user.username,
hashed_password=hashed_password
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.post("/login", response_model=Token)
def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
user = authenticate_user(db, user_credentials.email, user_credentials.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

21
app/routers/health.py Normal file
View File

@ -0,0 +1,21 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from sqlalchemy import text
from app.db.session import get_db
router = APIRouter()
@router.get("/health")
def health_check(db: Session = Depends(get_db)):
try:
db.execute(text("SELECT 1"))
database_status = "healthy"
except Exception as e:
database_status = f"unhealthy: {str(e)}"
return {
"status": "healthy" if database_status == "healthy" else "unhealthy",
"database": database_status,
"service": "Simple Messaging App"
}

91
app/routers/messages.py Normal file
View File

@ -0,0 +1,91 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.message import Message
from app.models.user import User
from app.schemas.message import MessageCreate, Message as MessageSchema
from app.auth.auth import get_current_active_user
router = APIRouter()
@router.post("/send", response_model=MessageSchema)
def send_message(
message: MessageCreate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
receiver = db.query(User).filter(User.id == message.receiver_id).first()
if not receiver:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Receiver not found"
)
if receiver.id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot send message to yourself"
)
db_message = Message(
sender_id=current_user.id,
receiver_id=message.receiver_id,
content=message.content
)
db.add(db_message)
db.commit()
db.refresh(db_message)
return db_message
@router.get("/received", response_model=List[MessageSchema])
def get_received_messages(
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
messages = db.query(Message).filter(
Message.receiver_id == current_user.id
).order_by(Message.created_at.desc()).all()
return messages
@router.get("/sent", response_model=List[MessageSchema])
def get_sent_messages(
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
messages = db.query(Message).filter(
Message.sender_id == current_user.id
).order_by(Message.created_at.desc()).all()
return messages
@router.get("/conversation/{user_id}", response_model=List[MessageSchema])
def get_conversation(
user_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
other_user = db.query(User).filter(User.id == user_id).first()
if not other_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
messages = db.query(Message).filter(
((Message.sender_id == current_user.id) & (Message.receiver_id == user_id)) |
((Message.sender_id == user_id) & (Message.receiver_id == current_user.id))
).order_by(Message.created_at.asc()).all()
return messages
@router.get("/users", response_model=List[dict])
def get_users(
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
users = db.query(User).filter(User.id != current_user.id).all()
return [{"id": user.id, "username": user.username, "email": user.email} for user in users]

0
app/schemas/__init__.py Normal file
View File

20
app/schemas/message.py Normal file
View File

@ -0,0 +1,20 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class MessageBase(BaseModel):
content: str
class MessageCreate(MessageBase):
receiver_id: int
class Message(MessageBase):
id: int
sender_id: int
receiver_id: int
created_at: datetime
sender: Optional[dict] = None
receiver: Optional[dict] = None
class Config:
from_attributes = True

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

@ -0,0 +1,30 @@
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class UserBase(BaseModel):
email: EmailStr
username: str
class UserCreate(UserBase):
password: str
class UserLogin(BaseModel):
email: EmailStr
password: str
class User(UserBase):
id: int
is_active: bool
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: Optional[str] = None

32
main.py Normal file
View File

@ -0,0 +1,32 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pathlib import Path
from app.routers import auth, messages, health
app = FastAPI(
title="Simple Messaging App",
description="A simple messaging application built with FastAPI",
version="1.0.0",
openapi_url="/openapi.json"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router, prefix="/auth", tags=["auth"])
app.include_router(messages.router, prefix="/messages", tags=["messages"])
app.include_router(health.router, tags=["health"])
@app.get("/")
async def root():
return {
"title": "Simple Messaging App",
"documentation": "/docs",
"health_check": "/health"
}

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
fastapi==0.104.1
uvicorn==0.24.0
sqlalchemy==2.0.23
alembic==1.12.1
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-decouple==3.8
ruff==0.1.6