async
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
database_url: str = "sqlite:///db.sqlite"
|
database_url: str = "sqlite+aiosqlite:///db.sqlite"
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
@@ -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
|
from app.core.config import settings
|
||||||
|
|
||||||
connect_args = {"check_same_thread": False} # SQLite-specific argument for multithreading
|
engine = create_async_engine(settings.database_url, echo=True, future=True)
|
||||||
engine = create_engine(settings.database_url, echo=True, connect_args=connect_args)
|
|
||||||
|
|
||||||
def get_session():
|
AsyncSessionLocal = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
with Session(engine) as session:
|
|
||||||
|
async def get_session():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
yield session
|
yield session
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from sqlmodel import SQLModel, Field, Session, select
|
from sqlmodel import select
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fido2.server import Fido2Server
|
from fido2.webauthn import PublicKeyCredentialDescriptor
|
||||||
from fido2.webauthn import PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, AttestationObject, AuthenticatorData
|
|
||||||
from fido2 import cbor
|
from fido2 import cbor
|
||||||
import secrets
|
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
from app.core.database import get_session
|
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.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.models import Admin, AdminCreate, AdminCredential, AdminFIDO2Challenge, TokenResponse
|
||||||
from app.internal.security import fido2_server
|
from app.internal.security import fido2_server
|
||||||
from app.utils.webauthn import convert_bytes_to_base64
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
async def register(
|
async def register(
|
||||||
user: AdminCreate,
|
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:
|
if existing is not None:
|
||||||
raise HTTPException(status_code=400, detail="Username already exists")
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
@@ -34,17 +31,17 @@ async def register(
|
|||||||
)
|
)
|
||||||
|
|
||||||
session.add(new_admin)
|
session.add(new_admin)
|
||||||
session.commit()
|
await session.commit()
|
||||||
session.refresh(new_admin)
|
await session.refresh(new_admin)
|
||||||
|
|
||||||
return JSONResponse(content={"message": "Admin registered successfully"}, status_code=201)
|
return JSONResponse(content={"message": "Admin registered successfully"}, status_code=201)
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login")
|
||||||
async def login(
|
async def login(
|
||||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||||
session: Session = Depends(get_session)
|
session: AsyncSession = Depends(get_session)
|
||||||
) -> TokenResponse:
|
) -> 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):
|
if not admin_user or not verify_password(form_data.password, admin_user.password):
|
||||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||||
@@ -58,7 +55,7 @@ async def login(
|
|||||||
@router.post("/register/begin")
|
@router.post("/register/begin")
|
||||||
async def register_begin(
|
async def register_begin(
|
||||||
current_admin: Admin = Depends(get_current_admin_user),
|
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(
|
registration_data, state = fido2_server.register_begin(
|
||||||
@@ -67,7 +64,7 @@ async def register_begin(
|
|||||||
"name": current_admin.username,
|
"name": current_admin.username,
|
||||||
"displayName": 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(
|
challenge = AdminFIDO2Challenge(
|
||||||
@@ -77,7 +74,7 @@ async def register_begin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
session.add(challenge)
|
session.add(challenge)
|
||||||
session.commit()
|
await session.commit()
|
||||||
|
|
||||||
print("Registration data:", registration_data)
|
print("Registration data:", registration_data)
|
||||||
return cbor.encode(registration_data).decode("utf-8")
|
return cbor.encode(registration_data).decode("utf-8")
|
||||||
@@ -87,12 +84,12 @@ async def register_begin(
|
|||||||
async def register_complete(
|
async def register_complete(
|
||||||
admin_id: str,
|
admin_id: str,
|
||||||
credential: dict,
|
credential: dict,
|
||||||
session: Session = Depends(get_session)
|
session: AsyncSession = Depends(get_session)
|
||||||
):
|
):
|
||||||
challenge = session.exec(
|
challenge = (await session.exec(
|
||||||
select(AdminFIDO2Challenge)
|
select(AdminFIDO2Challenge)
|
||||||
.where(AdminFIDO2Challenge.admin_id == int(admin_id), AdminFIDO2Challenge.type == "registration")
|
.where(AdminFIDO2Challenge.admin_id == int(admin_id), AdminFIDO2Challenge.type == "registration")
|
||||||
).first()
|
)).first()
|
||||||
|
|
||||||
if not challenge:
|
if not challenge:
|
||||||
raise HTTPException(status_code=400, detail="No registration challenge found")
|
raise HTTPException(status_code=400, detail="No registration challenge found")
|
||||||
@@ -102,7 +99,7 @@ async def register_complete(
|
|||||||
client_data=credential,
|
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(
|
new_credential = AdminCredential(
|
||||||
admin_id=user.id,
|
admin_id=user.id,
|
||||||
credential_id=auth_data.credential_id,
|
credential_id=auth_data.credential_id,
|
||||||
@@ -110,13 +107,13 @@ async def register_complete(
|
|||||||
sign_count=auth_data.sign_count
|
sign_count=auth_data.sign_count
|
||||||
)
|
)
|
||||||
session.add(new_credential)
|
session.add(new_credential)
|
||||||
session.delete(challenge)
|
await session.delete(challenge)
|
||||||
session.commit()
|
await session.commit()
|
||||||
|
|
||||||
return JSONResponse(content={"message": "FIDO2 registration successful"}, status_code=200)
|
return JSONResponse(content={"message": "FIDO2 registration successful"}, status_code=200)
|
||||||
|
|
||||||
@router.post("/login/begin")
|
@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()
|
admin_user = session.exec(select(Admin).where(Admin.username == username)).first()
|
||||||
if not admin_user:
|
if not admin_user:
|
||||||
raise HTTPException(status_code=400, detail="User not found")
|
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.add(challenge)
|
||||||
session.commit()
|
await session.commit()
|
||||||
|
|
||||||
return authentication_data
|
return authentication_data
|
||||||
|
|
||||||
@router.post("/login/complete")
|
@router.post("/login/complete")
|
||||||
async def login_complete(admin_id: str, credential: dict, session: Session = Depends(get_session)):
|
async def login_complete(admin_id: str, credential: dict, session: AsyncSession = Depends(get_session)):
|
||||||
challenge = session.exec(
|
challenge = (await session.exec(
|
||||||
select(AdminFIDO2Challenge)
|
select(AdminFIDO2Challenge)
|
||||||
.where(AdminFIDO2Challenge.admin_id == int(admin_id))
|
.where(AdminFIDO2Challenge.admin_id == int(admin_id))
|
||||||
.where(AdminFIDO2Challenge.type == "authentication")
|
.where(AdminFIDO2Challenge.type == "authentication")
|
||||||
).first()
|
)).first()
|
||||||
|
|
||||||
if not challenge:
|
if not challenge:
|
||||||
raise HTTPException(status_code=400, detail="No authentication challenge found")
|
raise HTTPException(status_code=400, detail="No authentication challenge found")
|
||||||
|
|
||||||
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()
|
||||||
credentials = session.exec(select(AdminCredential).where(AdminCredential.admin_id == user.id)).all()
|
credentials = (await session.exec(select(AdminCredential).where(AdminCredential.admin_id == user.id))).all()
|
||||||
|
|
||||||
auth_data = fido2_server.authenticate_complete(
|
auth_data = fido2_server.authenticate_complete(
|
||||||
state={"challenge": challenge.challenge},
|
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)
|
select(AdminCredential)
|
||||||
.where(AdminCredential.credential_id == auth_data.credential_id)
|
.where(AdminCredential.credential_id == auth_data.credential_id)
|
||||||
).first()
|
)).first()
|
||||||
credential.sign_count = auth_data.sign_count
|
credential.sign_count = auth_data.sign_count
|
||||||
session.delete(challenge)
|
await session.delete(challenge)
|
||||||
session.commit()
|
await session.commit()
|
||||||
|
|
||||||
return JSONResponse(content={"message": "FIDO2 authentication successful"}, status_code=200)
|
return JSONResponse(content={"message": "FIDO2 authentication successful"}, status_code=200)
|
||||||
@@ -5,9 +5,11 @@ description = "Add your description here"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aiosqlite>=0.22.1",
|
||||||
"alembic>=1.18.4",
|
"alembic>=1.18.4",
|
||||||
"fastapi[standard-no-fastapi-cloud-cli]>=0.129.0",
|
"fastapi[standard-no-fastapi-cloud-cli]>=0.129.0",
|
||||||
"fido2>=2.1.1",
|
"fido2>=2.1.1",
|
||||||
|
"greenlet>=3.3.1",
|
||||||
"pwdlib[argon2]>=0.3.0",
|
"pwdlib[argon2]>=0.3.0",
|
||||||
"pydantic-settings>=2.13.0",
|
"pydantic-settings>=2.13.0",
|
||||||
"pyjwt[crypto]>=2.11.0",
|
"pyjwt[crypto]>=2.11.0",
|
||||||
|
|||||||
13
uv.lock
generated
13
uv.lock
generated
@@ -6,6 +6,15 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.14'",
|
"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]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.18.4"
|
version = "1.18.4"
|
||||||
@@ -295,9 +304,11 @@ name = "fastcon-backend"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "aiosqlite" },
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
{ name = "fastapi", extra = ["standard-no-fastapi-cloud-cli"] },
|
{ name = "fastapi", extra = ["standard-no-fastapi-cloud-cli"] },
|
||||||
{ name = "fido2" },
|
{ name = "fido2" },
|
||||||
|
{ name = "greenlet" },
|
||||||
{ name = "pwdlib", extra = ["argon2"] },
|
{ name = "pwdlib", extra = ["argon2"] },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyjwt", extra = ["crypto"] },
|
{ name = "pyjwt", extra = ["crypto"] },
|
||||||
@@ -307,9 +318,11 @@ dependencies = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "aiosqlite", specifier = ">=0.22.1" },
|
||||||
{ name = "alembic", specifier = ">=1.18.4" },
|
{ name = "alembic", specifier = ">=1.18.4" },
|
||||||
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.129.0" },
|
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.129.0" },
|
||||||
{ name = "fido2", specifier = ">=2.1.1" },
|
{ name = "fido2", specifier = ">=2.1.1" },
|
||||||
|
{ name = "greenlet", specifier = ">=3.3.1" },
|
||||||
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" },
|
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.13.0" },
|
{ name = "pydantic-settings", specifier = ">=2.13.0" },
|
||||||
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.11.0" },
|
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.11.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user