Initial commit from template

This commit is contained in:
Obi Madu 2025-03-20 02:41:34 +01:00
commit 2e591f5f61
11 changed files with 257 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Python
__pycache__/
*.py[cod]
*$py.class
# Environment
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Logs
*.log
# Local development
.settings/
.vscode/
# Database
*.db
*.sqlite
# Temporary files
*.tmp
*.temp
# Security
secrets.json
credentials.json
# IDE specific
.idea/

0
core/__init__.py Normal file
View File

53
core/auth.py Normal file
View File

@ -0,0 +1,53 @@
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from datetime import datetime, timedelta
from passlib.context import CryptContext
from models.user import User
from core.database import get_db
from sqlalchemy.orm import Session
from typing import Optional
# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = "demo-secret-key"
ALGORITHM = "HS256"
def get_password_hash(password: str):
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)
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})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user_demo(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
):
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials"
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise credentials_exception
return user

25
core/database.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
DB_DIR = BASE_DIR / "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}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

18
core/router.py Normal file
View File

@ -0,0 +1,18 @@
import importlib.util
from pathlib import Path
from fastapi import APIRouter
def load_endpoints(base_path: Path = Path("endpoints")) -> APIRouter:
router = APIRouter()
for file_path in base_path.glob("**/*.*.py"):
# Load the module
spec = importlib.util.spec_from_file_location("", file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Find the router in the module and include it directly
if hasattr(module, "router"):
router.include_router(module.router)
return router

0
endpoints/__init__.py Normal file
View File

11
endpoints/health.get.py Normal file
View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "ok",
"message": "Service is healthy"
}

37
endpoints/login.post.py Normal file
View File

@ -0,0 +1,37 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from datetime import timedelta
from core.database import get_db
from sqlalchemy.orm import Session
from core.auth import verify_password, create_access_token
from models.user import User
router = APIRouter()
class UserAuth(BaseModel):
username: str
password: str
@router.post("/login")
async def login(
user_data: UserAuth,
db: Session = Depends(get_db)
):
"""User authentication endpoint"""
user = db.query(User).filter(User.username == user_data.username).first()
if not user or not verify_password(user_data.password, user.hashed_password):
raise HTTPException(status_code=400, detail="Invalid credentials")
# Generate token with expiration
access_token = create_access_token(
data={"sub": user.id},
expires_delta=timedelta(hours=1)
)
return {
"access_token": access_token,
"token_type": "bearer",
"user_id": user.id,
"username": user.username
}

50
endpoints/signup.post.py Normal file
View File

@ -0,0 +1,50 @@
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from pydantic import BaseModel
from core.database import get_db
from core.auth import get_password_hash, create_access_token
import uuid
from models.user import User
router = APIRouter()
class UserCreate(BaseModel):
username: str
email: str
password: str
@router.post("/signup")
async def signup(
user_data: UserCreate,
db: Session = Depends(get_db)
):
"""User registration endpoint"""
# Check existing user
db_user = db.query(User).filter(
(User.username == user_data.username) |
(User.email == user_data.email)
).first()
if db_user:
raise HTTPException(
status_code=400,
detail="Username or email already exists"
)
# Create new user
new_user = User(
id=str(uuid.uuid4()),
username=user_data.username,
email=user_data.email,
hashed_password=get_password_hash(user_data.password)
)
db.add(new_user)
db.commit()
# Return token directly after registration
return {
"message": "User created successfully",
"access_token": create_access_token({"sub": new_user.id}),
"token_type": "bearer"
}

19
main.py Normal file
View File

@ -0,0 +1,19 @@
from fastapi import FastAPI
from pathlib import Path
from core.router import load_endpoints
app = FastAPI(title="API Starter Template")
# Load all endpoints
app.include_router(load_endpoints(Path("endpoints")))
@app.get("/")
def root():
return {
"message": "FastAPI App Running",
"endpoints": {
"health": "/health (GET)",
"login": "/login (POST)",
"signup": "/signup (POST)"
}
}

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
fastapi>=0.68.0
uvicorn[standard]
python-multipart
python-jose[cryptography]
passlib[bcrypt]
sqlalchemy>=1.4.0
python-dotenv>=0.19.0
bcrypt>=3.2.0
alembic>=1.13.1