From ed8ba1ae7da40b58dab9dd8c77116251c95297dd Mon Sep 17 00:00:00 2001 From: NGYF <1605146531@qq.com> Date: Sun, 22 Feb 2026 00:05:04 +0800 Subject: [PATCH] async --- app/core/config.py | 2 +- app/core/database.py | 12 ++++---- app/internal/routers.py | 65 ++++++++++++++++++++--------------------- pyproject.toml | 2 ++ uv.lock | 13 +++++++++ 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index 327ec9b..58dd1c4 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,6 +1,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): - database_url: str = "sqlite:///db.sqlite" + database_url: str = "sqlite+aiosqlite:///db.sqlite" settings = Settings() \ No newline at end of file diff --git a/app/core/database.py b/app/core/database.py index 6800968..5f8607b 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -1,9 +1,11 @@ -from sqlmodel import Session, create_engine +from sqlmodel.ext.asyncio.session import AsyncSession +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from app.core.config import settings -connect_args = {"check_same_thread": False} # SQLite-specific argument for multithreading -engine = create_engine(settings.database_url, echo=True, connect_args=connect_args) +engine = create_async_engine(settings.database_url, echo=True, future=True) -def get_session(): - with Session(engine) as session: +AsyncSessionLocal = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) + +async def get_session(): + async with AsyncSessionLocal() as session: yield session \ No newline at end of file diff --git a/app/internal/routers.py b/app/internal/routers.py index 5fa2f4c..3ef97d1 100644 --- a/app/internal/routers.py +++ b/app/internal/routers.py @@ -1,29 +1,26 @@ -from datetime import datetime, timezone from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordRequestForm -from sqlmodel import SQLModel, Field, Session, select -from fastapi import APIRouter, Depends, HTTPException, Request -from fido2.server import Fido2Server -from fido2.webauthn import PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, AttestationObject, AuthenticatorData +from sqlmodel import select +from fastapi import APIRouter, Depends, HTTPException +from fido2.webauthn import PublicKeyCredentialDescriptor from fido2 import cbor -import secrets + +from sqlmodel.ext.asyncio.session import AsyncSession from app.core.database import get_session -from typing import Any from app.internal.auth import create_access_token, get_current_admin_user, hash_password, verify_password from app.internal.models import Admin, AdminCreate, AdminCredential, AdminFIDO2Challenge, TokenResponse from app.internal.security import fido2_server -from app.utils.webauthn import convert_bytes_to_base64 router = APIRouter(prefix="/admin", tags=["admin"]) @router.post("/register") async def register( user: AdminCreate, - session: Session = Depends(get_session) + session: AsyncSession = Depends(get_session) ): - existing = session.exec(select(Admin).where(Admin.username == user.username)).first() + existing = (await session.exec(select(Admin).where(Admin.username == user.username))).first() if existing is not None: raise HTTPException(status_code=400, detail="Username already exists") @@ -34,17 +31,17 @@ async def register( ) session.add(new_admin) - session.commit() - session.refresh(new_admin) + await session.commit() + await session.refresh(new_admin) return JSONResponse(content={"message": "Admin registered successfully"}, status_code=201) @router.post("/login") async def login( form_data: OAuth2PasswordRequestForm = Depends(), - session: Session = Depends(get_session) + session: AsyncSession = Depends(get_session) ) -> TokenResponse: - admin_user = session.exec(select(Admin).where(Admin.username == form_data.username)).first() + admin_user = (await session.exec(select(Admin).where(Admin.username == form_data.username))).first() if not admin_user or not verify_password(form_data.password, admin_user.password): raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -58,7 +55,7 @@ async def login( @router.post("/register/begin") async def register_begin( current_admin: Admin = Depends(get_current_admin_user), - session: Session = Depends(get_session) + session: AsyncSession = Depends(get_session) ): registration_data, state = fido2_server.register_begin( @@ -67,7 +64,7 @@ async def register_begin( "name": current_admin.username, "displayName": current_admin.username }, - credentials=session.exec(select(AdminCredential).where(AdminCredential.admin_id == current_admin.id)).all(), + credentials=(await session.exec(select(AdminCredential).where(AdminCredential.admin_id == current_admin.id))).all(), ) challenge = AdminFIDO2Challenge( @@ -77,7 +74,7 @@ async def register_begin( ) session.add(challenge) - session.commit() + await session.commit() print("Registration data:", registration_data) return cbor.encode(registration_data).decode("utf-8") @@ -87,12 +84,12 @@ async def register_begin( async def register_complete( admin_id: str, credential: dict, - session: Session = Depends(get_session) + session: AsyncSession = Depends(get_session) ): - challenge = session.exec( + challenge = (await session.exec( select(AdminFIDO2Challenge) .where(AdminFIDO2Challenge.admin_id == int(admin_id), AdminFIDO2Challenge.type == "registration") - ).first() + )).first() if not challenge: raise HTTPException(status_code=400, detail="No registration challenge found") @@ -102,7 +99,7 @@ async def register_complete( client_data=credential, ) - user = session.exec(select(Admin).where(Admin.id == int(admin_id))).first() + user = (await session.exec(select(Admin).where(Admin.id == int(admin_id)))).first() new_credential = AdminCredential( admin_id=user.id, credential_id=auth_data.credential_id, @@ -110,13 +107,13 @@ async def register_complete( sign_count=auth_data.sign_count ) session.add(new_credential) - session.delete(challenge) - session.commit() + await session.delete(challenge) + await session.commit() return JSONResponse(content={"message": "FIDO2 registration successful"}, status_code=200) @router.post("/login/begin") -async def login_begin(username: str, session: Session = Depends(get_session)): +async def login_begin(username: str, session: AsyncSession = Depends(get_session)): admin_user = session.exec(select(Admin).where(Admin.username == username)).first() if not admin_user: raise HTTPException(status_code=400, detail="User not found") @@ -140,23 +137,23 @@ async def login_begin(username: str, session: Session = Depends(get_session)): ) session.add(challenge) - session.commit() + await session.commit() return authentication_data @router.post("/login/complete") -async def login_complete(admin_id: str, credential: dict, session: Session = Depends(get_session)): - challenge = session.exec( +async def login_complete(admin_id: str, credential: dict, session: AsyncSession = Depends(get_session)): + challenge = (await session.exec( select(AdminFIDO2Challenge) .where(AdminFIDO2Challenge.admin_id == int(admin_id)) .where(AdminFIDO2Challenge.type == "authentication") - ).first() + )).first() if not challenge: raise HTTPException(status_code=400, detail="No authentication challenge found") - user = session.exec(select(Admin).where(Admin.id == int(admin_id))).first() - credentials = session.exec(select(AdminCredential).where(AdminCredential.admin_id == user.id)).all() + user = (await session.exec(select(Admin).where(Admin.id == int(admin_id)))).first() + credentials = (await session.exec(select(AdminCredential).where(AdminCredential.admin_id == user.id))).all() auth_data = fido2_server.authenticate_complete( state={"challenge": challenge.challenge}, @@ -170,12 +167,12 @@ async def login_complete(admin_id: str, credential: dict, session: Session = Dep ], ) - credential = session.exec( + credential = (await session.exec( select(AdminCredential) .where(AdminCredential.credential_id == auth_data.credential_id) - ).first() + )).first() credential.sign_count = auth_data.sign_count - session.delete(challenge) - session.commit() + await session.delete(challenge) + await session.commit() return JSONResponse(content={"message": "FIDO2 authentication successful"}, status_code=200) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cbf9a3c..c14ae39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,9 +5,11 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "aiosqlite>=0.22.1", "alembic>=1.18.4", "fastapi[standard-no-fastapi-cloud-cli]>=0.129.0", "fido2>=2.1.1", + "greenlet>=3.3.1", "pwdlib[argon2]>=0.3.0", "pydantic-settings>=2.13.0", "pyjwt[crypto]>=2.11.0", diff --git a/uv.lock b/uv.lock index 74df91f..e013ffb 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,15 @@ resolution-markers = [ "python_full_version < '3.14'", ] +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + [[package]] name = "alembic" version = "1.18.4" @@ -295,9 +304,11 @@ name = "fastcon-backend" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "aiosqlite" }, { name = "alembic" }, { name = "fastapi", extra = ["standard-no-fastapi-cloud-cli"] }, { name = "fido2" }, + { name = "greenlet" }, { name = "pwdlib", extra = ["argon2"] }, { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, @@ -307,9 +318,11 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "aiosqlite", specifier = ">=0.22.1" }, { name = "alembic", specifier = ">=1.18.4" }, { name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.129.0" }, { name = "fido2", specifier = ">=2.1.1" }, + { name = "greenlet", specifier = ">=3.3.1" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" }, { name = "pydantic-settings", specifier = ">=2.13.0" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.11.0" },