first commit

This commit is contained in:
xuyong
2026-04-15 21:35:26 +08:00
commit 7097fa6b44
69 changed files with 5642 additions and 0 deletions

0
app/core/__init__.py Normal file
View File

21
app/core/database.py Normal file
View File

@@ -0,0 +1,21 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase
from app.config.settings import settings
engine = create_async_engine(settings.database_url, echo=settings.debug)
async_session = async_sessionmaker(engine, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def get_db() -> AsyncSession:
async with async_session() as session:
yield session
async def init_db():
import app.models # noqa: F401 — ensure all models are registered
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

25
app/core/dependencies.py Normal file
View File

@@ -0,0 +1,25 @@
from fastapi import Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db # noqa: F401
from app.core.security import decode_token
from app.core.exceptions import UnauthorizedError
from app.models import User
security_scheme = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
payload = decode_token(credentials.credentials)
if not payload or payload.get("type") != "access":
raise UnauthorizedError("Invalid or expired token")
user_id = payload.get("sub")
user = await db.get(User, user_id)
if not user or user.status != "active":
raise UnauthorizedError("User not found or disabled")
return user

21
app/core/exceptions.py Normal file
View File

@@ -0,0 +1,21 @@
from fastapi import HTTPException
class NotFoundError(HTTPException):
def __init__(self, detail: str = "Resource not found"):
super().__init__(status_code=404, detail=detail)
class BadRequestError(HTTPException):
def __init__(self, detail: str = "Bad request"):
super().__init__(status_code=400, detail=detail)
class UnauthorizedError(HTTPException):
def __init__(self, detail: str = "Not authenticated"):
super().__init__(status_code=401, detail=detail, headers={"WWW-Authenticate": "Bearer"})
class ForbiddenError(HTTPException):
def __init__(self, detail: str = "Forbidden"):
super().__init__(status_code=403, detail=detail)

40
app/core/security.py Normal file
View File

@@ -0,0 +1,40 @@
from datetime import datetime, timedelta
from typing import Optional
import jwt
import bcrypt
from app.config.settings import settings
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
def create_access_token(user_id: str) -> str:
expire = datetime.utcnow() + timedelta(minutes=settings.jwt_access_expire_minutes)
payload = {"sub": user_id, "exp": expire, "type": "access"}
return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
def create_refresh_token(user_id: str) -> str:
expire = datetime.utcnow() + timedelta(days=settings.jwt_refresh_expire_days)
payload = {"sub": user_id, "exp": expire, "type": "refresh"}
return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
def create_reset_token(user_id: str) -> str:
expire = datetime.utcnow() + timedelta(hours=1)
payload = {"sub": user_id, "exp": expire, "type": "reset"}
return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
def decode_token(token: str) -> Optional[dict]:
try:
return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm])
except jwt.PyJWTError:
return None