Restructure into full project layout
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import select
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.core.security import hash_password
|
||||
from app.core.config import settings
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
|
||||
async def bootstrap_admin():
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(select(User).where(User.email == settings.ADMIN_EMAIL))
|
||||
if result.scalar_one_or_none():
|
||||
return
|
||||
|
||||
admin = User(
|
||||
email=settings.ADMIN_EMAIL,
|
||||
hashed_password=hash_password(settings.ADMIN_PASSWORD),
|
||||
display_name="Admin",
|
||||
role=UserRole.ADMIN,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(admin)
|
||||
await db.commit()
|
||||
print(f"Admin user created: {settings.ADMIN_EMAIL}")
|
||||
@@ -0,0 +1,26 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
SECRET_KEY: str
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
|
||||
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
|
||||
|
||||
ANTHROPIC_API_KEY: str
|
||||
ANTHROPIC_MODEL: str = "claude-sonnet-4-6"
|
||||
|
||||
ADMIN_EMAIL: str
|
||||
ADMIN_PASSWORD: str
|
||||
|
||||
DATABASE_URL: str
|
||||
REDIS_URL: str = "redis://cache:6379"
|
||||
|
||||
SCRYFALL_RATE_LIMIT_RPS: int = 8
|
||||
SCRYFALL_CACHE_TTL_SECONDS: int = 86400
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
@@ -0,0 +1,37 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_token
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
bearer = HTTPBearer()
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(bearer),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User:
|
||||
user_id = decode_token(credentials.credentials)
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
async def get_approved_user(user: User = Depends(get_current_user)) -> User:
|
||||
if user.role == UserRole.PENDING:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account pending approval")
|
||||
return user
|
||||
|
||||
|
||||
async def get_admin_user(user: User = Depends(get_approved_user)) -> User:
|
||||
if user.role != UserRole.ADMIN:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
|
||||
return user
|
||||
@@ -0,0 +1,11 @@
|
||||
import redis.asyncio as redis
|
||||
from app.core.config import settings
|
||||
|
||||
_redis = None
|
||||
|
||||
|
||||
async def get_redis():
|
||||
global _redis
|
||||
if _redis is None:
|
||||
_redis = redis.from_url(settings.REDIS_URL, decode_responses=True)
|
||||
return _redis
|
||||
@@ -0,0 +1,43 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(plain, hashed)
|
||||
|
||||
|
||||
def create_access_token(subject: int) -> str:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return jwt.encode(
|
||||
{"sub": str(subject), "exp": expire, "type": "access"},
|
||||
settings.SECRET_KEY,
|
||||
algorithm=settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def create_refresh_token(subject: int) -> str:
|
||||
expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
return jwt.encode(
|
||||
{"sub": str(subject), "exp": expire, "type": "refresh"},
|
||||
settings.SECRET_KEY,
|
||||
algorithm=settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def decode_token(token: str) -> Optional[int]:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return int(payload["sub"])
|
||||
except (JWTError, KeyError, ValueError):
|
||||
return None
|
||||
Reference in New Issue
Block a user