feat: implement one-time token

This commit is contained in:
julius 2025-03-11 13:37:23 +01:00
parent 1067b12be8
commit 6eb2563068
Signed by: julius
GPG Key ID: C80A63E6A5FD7092
3 changed files with 57 additions and 23 deletions

8
db.py
View File

@ -73,4 +73,12 @@ class MVPRanking(SQLModel, table=True):
mvps: list[int] = Field(sa_column=Column(ARRAY(Integer))) mvps: list[int] = Field(sa_column=Column(ARRAY(Integer)))
class TokenDB(SQLModel, table=True):
token: str = Field(index=True, primary_key=True)
used: bool | None = False
updated_at: datetime | None = Field(
default_factory=utctime, sa_column_kwargs={"onupdate": utctime}
)
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

View File

@ -6,7 +6,7 @@ from pydantic import BaseModel, ValidationError
import jwt import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from sqlmodel import Session, select from sqlmodel import Session, select
from db import engine, Player from db import TokenDB, engine, Player
from fastapi.security import ( from fastapi.security import (
OAuth2PasswordBearer, OAuth2PasswordBearer,
OAuth2PasswordRequestForm, OAuth2PasswordRequestForm,
@ -178,6 +178,7 @@ async def login_for_access_token(
value=access_token, value=access_token,
httponly=True, httponly=True,
samesite="strict", samesite="strict",
max_age=config.access_token_expire_minutes * 60,
) )
return Token(access_token=access_token) return Token(access_token=access_token)
@ -208,26 +209,51 @@ async def set_first_password(req: FirstPassword):
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate token", detail="Could not validate token",
) )
try: with Session(engine) as session:
payload = jwt.decode(req.token, config.secret_key, algorithms=["HS256"]) token_in_db = session.exec(
username: str = payload.get("sub") select(TokenDB)
if username is None: .where(TokenDB.token == req.token)
raise credentials_exception .where(TokenDB.used == False)
except ExpiredSignatureError: ).one_or_none()
raise HTTPException( if token_in_db:
status_code=status.HTTP_401_UNAUTHORIZED, credentials_exception = HTTPException(
detail="Access token expired", status_code=status.HTTP_401_UNAUTHORIZED,
) detail="Could not validate token",
except (InvalidTokenError, ValidationError): )
raise credentials_exception try:
payload = jwt.decode(req.token, config.secret_key, algorithms=["HS256"])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Access token expired",
)
except (InvalidTokenError, ValidationError):
raise credentials_exception
user = get_user(username) user = get_user(username)
if user: if user:
with Session(engine) as session: user.hashed_password = get_password_hash(req.password)
user.hashed_password = get_password_hash(req.password) session.add(user)
session.add(user) token_in_db.used = True
session.commit() session.add(token_in_db)
return Response("Password set successfully", status_code=status.HTTP_200_OK) session.commit()
return Response(
"Password set successfully", status_code=status.HTTP_200_OK
)
elif session.exec(
select(TokenDB)
.where(TokenDB.token == req.token)
.where(TokenDB.used == True)
).one_or_none():
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token already used",
)
else:
raise credentials_exception
async def change_password( async def change_password(

View File

@ -63,9 +63,9 @@ export const SetPassword = () => {
if (!resp.ok) { if (!resp.ok) {
if (resp.status === 401) { if (resp.status === 401) {
resp.statusText const { detail } = await resp.json();
? setError(resp.statusText) if (detail) setError(detail);
: setError("unauthorized"); else setError("unauthorized");
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
} }