first commit

This commit is contained in:
Michael
2026-05-27 14:37:46 +02:00
commit 1b9be90d29
24 changed files with 778 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
View File
+12
View File
@@ -0,0 +1,12 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "postgresql+asyncpg://pastebin:pastebin@db:5432/pastebin"
admin_password: str = "change-me"
max_paste_length: int = 100_000
model_config = {"env_file": ".env"}
settings = Settings()
+16
View File
@@ -0,0 +1,16 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from app.config import settings
engine = create_async_engine(settings.database_url, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def get_db() -> AsyncSession:
async with async_session() as session:
yield session
+89
View File
@@ -0,0 +1,89 @@
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException, Query, Header
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database import get_db, engine, Base
from app.models import Paste
from app.schemas import (
PasteCreate,
PasteResponse,
PasteCreatedResponse,
PasteListResponse,
)
@asynccontextmanager
async def lifespan(app: FastAPI):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
app = FastAPI(title="Pastebin API", lifespan=lifespan)
def verify_admin(password: str = Header(..., alias="X-Admin-Password")):
if password != settings.admin_password:
raise HTTPException(status_code=401, detail="Invalid admin password")
@app.post("/api/pastes", response_model=PasteCreatedResponse, status_code=201)
async def create_paste(data: PasteCreate, db: AsyncSession = Depends(get_db)):
paste = Paste(content=data.content)
db.add(paste)
await db.commit()
await db.refresh(paste)
return PasteCreatedResponse(
id=paste.id, delete_token=paste.delete_token, created_at=paste.created_at
)
@app.get("/api/pastes/{paste_id}", response_model=PasteResponse)
async def get_paste(paste_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
paste = await db.get(Paste, paste_id)
if not paste:
raise HTTPException(status_code=404, detail="Paste not found")
return PasteResponse(id=paste.id, content=paste.content, created_at=paste.created_at)
@app.delete("/api/pastes/{paste_id}")
async def delete_paste(
paste_id: uuid.UUID,
token: uuid.UUID = Query(..., alias="token"),
db: AsyncSession = Depends(get_db),
):
paste = await db.get(Paste, paste_id)
if not paste:
raise HTTPException(status_code=404, detail="Paste not found")
if paste.delete_token != token:
raise HTTPException(status_code=403, detail="Invalid delete token")
await db.delete(paste)
await db.commit()
return {"detail": "Paste deleted"}
@app.get("/api/admin/pastes", response_model=list[PasteListResponse])
async def list_pastes(
_=Depends(verify_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Paste).order_by(Paste.created_at.desc()))
return result.scalars().all()
@app.delete("/api/admin/pastes/{paste_id}")
async def admin_delete_paste(
paste_id: uuid.UUID,
_=Depends(verify_admin),
db: AsyncSession = Depends(get_db),
):
paste = await db.get(Paste, paste_id)
if not paste:
raise HTTPException(status_code=404, detail="Paste not found")
await db.delete(paste)
await db.commit()
return {"detail": "Paste deleted"}
+23
View File
@@ -0,0 +1,23 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import String, Text, DateTime
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class Paste(Base):
__tablename__ = "pastes"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
content: Mapped[str] = mapped_column(Text, nullable=False)
delete_token: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), nullable=False, default=uuid.uuid4
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
+31
View File
@@ -0,0 +1,31 @@
import uuid
from datetime import datetime
from pydantic import BaseModel, Field
from app.config import settings
class PasteCreate(BaseModel):
content: str = Field(..., min_length=1, max_length=settings.max_paste_length)
class PasteResponse(BaseModel):
id: uuid.UUID
content: str
created_at: datetime
model_config = {"from_attributes": True}
class PasteCreatedResponse(BaseModel):
id: uuid.UUID
delete_token: uuid.UUID
created_at: datetime
class PasteListResponse(BaseModel):
id: uuid.UUID
created_at: datetime
model_config = {"from_attributes": True}
+9
View File
@@ -0,0 +1,9 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
sqlalchemy==2.0.36
asyncpg==0.30.0
psycopg2-binary==2.9.10
pydantic==2.10.3
pydantic-settings==2.7.0
python-dotenv==1.0.1
alembic==1.14.0