From 2e591f5f61dd9773de0a537d74758e3f1b68ca30 Mon Sep 17 00:00:00 2001 From: Obi Madu Date: Thu, 20 Mar 2025 02:41:34 +0100 Subject: [PATCH] Initial commit from template --- .gitignore | 35 ++++++++++++++++++++++++++ core/__init__.py | 0 core/auth.py | 53 ++++++++++++++++++++++++++++++++++++++++ core/database.py | 25 +++++++++++++++++++ core/router.py | 18 ++++++++++++++ endpoints/__init__.py | 0 endpoints/health.get.py | 11 +++++++++ endpoints/login.post.py | 37 ++++++++++++++++++++++++++++ endpoints/signup.post.py | 50 +++++++++++++++++++++++++++++++++++++ main.py | 19 ++++++++++++++ requirements.txt | 9 +++++++ 11 files changed, 257 insertions(+) create mode 100644 .gitignore create mode 100644 core/__init__.py create mode 100644 core/auth.py create mode 100644 core/database.py create mode 100644 core/router.py create mode 100644 endpoints/__init__.py create mode 100644 endpoints/health.get.py create mode 100644 endpoints/login.post.py create mode 100644 endpoints/signup.post.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8765460 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/auth.py b/core/auth.py new file mode 100644 index 0000000..18ea9a6 --- /dev/null +++ b/core/auth.py @@ -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 diff --git a/core/database.py b/core/database.py new file mode 100644 index 0000000..762c135 --- /dev/null +++ b/core/database.py @@ -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() diff --git a/core/router.py b/core/router.py new file mode 100644 index 0000000..40ae97d --- /dev/null +++ b/core/router.py @@ -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 diff --git a/endpoints/__init__.py b/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/endpoints/health.get.py b/endpoints/health.get.py new file mode 100644 index 0000000..1f50c77 --- /dev/null +++ b/endpoints/health.get.py @@ -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" + } diff --git a/endpoints/login.post.py b/endpoints/login.post.py new file mode 100644 index 0000000..a8ab4aa --- /dev/null +++ b/endpoints/login.post.py @@ -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 + } diff --git a/endpoints/signup.post.py b/endpoints/signup.post.py new file mode 100644 index 0000000..77e4d09 --- /dev/null +++ b/endpoints/signup.post.py @@ -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" + } diff --git a/main.py b/main.py new file mode 100644 index 0000000..297cc87 --- /dev/null +++ b/main.py @@ -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)" + } + } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..596e6f3 --- /dev/null +++ b/requirements.txt @@ -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