commit e07bf26000ee3e2578e7aff7c301e0fe2afaf661 Author: Backend IM Bot Date: Mon Apr 28 13:03:00 2025 +0000 Initial commit from template diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..67676e2 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,27 @@ +[alembic] +script_location = alembic +sqlalchemy.url = sqlite:///./storage/db/db.sqlite + +[loggers] +keys = root + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[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 diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..abf21c6 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,48 @@ +from logging.config import fileConfig +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from alembic import context +import sys +import os + +# Add project root to Python path +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from core.database import Base, SQLALCHEMY_DATABASE_URL +from models.user import User # Import all models + +config = context.config +fileConfig(config.config_file_name) +config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL) + +target_metadata = Base.metadata + +def run_migrations_offline(): + context.configure( + url=SQLALCHEMY_DATABASE_URL, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online(): + connectable = engine_from_config( + {"sqlalchemy.url": SQLALCHEMY_DATABASE_URL}, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True + ) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..535780d --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,22 @@ +"""${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(): + ${upgrades if upgrades else "pass"} + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/0001_initial.py b/alembic/versions/0001_initial.py new file mode 100644 index 0000000..3e1202f --- /dev/null +++ b/alembic/versions/0001_initial.py @@ -0,0 +1,31 @@ +"""Initial migration + +Revision ID: 0001 +Revises: +Create Date: 2024-01-01 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + +revision = '0001' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table('users', + sa.Column('id', sa.String(), nullable=False), + sa.Column('username', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('hashed_password', sa.String(), nullable=False), + sa.Column('disabled', sa.Boolean(), server_default='false', nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) + +def downgrade(): + op.drop_index(op.f('ix_users_username'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') 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..05ebf88 --- /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() \ No newline at end of file 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/helpers/__init__.py b/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..7b72c0d --- /dev/null +++ b/main.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware # New import +from pathlib import Path +from core.router import load_endpoints + +app = FastAPI(title="API Starter Template") + +# Add CORS middleware configuration +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers +) + +# 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)" + } + } \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..faf81eb --- /dev/null +++ b/models/user.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, String, Boolean +from core.database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(String, primary_key=True) + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + disabled = Column(Boolean, default=False) 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 diff --git a/schemas/__init__.py b/schemas/__init__.py new file mode 100644 index 0000000..e69de29