user creation flow with altcha verification

This commit is contained in:
Jack Case
2025-10-20 18:25:29 +00:00
parent 783c522234
commit cc8d4e9977
4 changed files with 54 additions and 6 deletions

1
slopserver/common.py Normal file
View File

@@ -0,0 +1 @@
TEMP_HMAC_KEY = "0460de065912d0292df1e7422a5ed2dc362ed56d6bab64fe50b89957463061f3"

View File

@@ -51,3 +51,10 @@ def get_user(email, engine):
with Session(engine) as session: with Session(engine) as session:
user = session.scalar(query) user = session.scalar(query)
return user return user
def create_user(email, password_hash, engine):
user = User(email=email, password_hash=password_hash, email_verified=False)
with Session(engine) as session:
session.add(user)
session.commit()

View File

@@ -1,9 +1,13 @@
from typing import Annotated from typing import Annotated
from sqlmodel import Field, SQLModel, create_engine, Relationship from sqlmodel import Field, SQLModel, create_engine, Relationship
from pydantic import AfterValidator, BaseModel from pydantic import AfterValidator, BaseModel, EmailStr, Json
from altcha import Payload as AltchaPayload, verify_solution
from urllib.parse import urlparse, ParseResult from urllib.parse import urlparse, ParseResult
from slopserver.common import TEMP_HMAC_KEY
NAMING_CONVENTION = { NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s", "ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s", "uq": "uq_%(table_name)s_%(column_0_name)s",
@@ -36,7 +40,6 @@ class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
email: str = Field(index=True, unique=True) email: str = Field(index=True, unique=True)
password_hash: str password_hash: str
salt: str
email_verified: bool = Field(default=False) email_verified: bool = Field(default=False)
@@ -56,7 +59,17 @@ def url_validator(urls: list[str]) -> list[ParseResult]:
raise ValueError(f"couldn't parse '{url}' as a URL") raise ValueError(f"couldn't parse '{url}' as a URL")
return parsed_urls return parsed_urls
def altcha_validator(altcha_response: AltchaPayload):
verified = verify_solution(altcha_response, TEMP_HMAC_KEY)
if not verified[0]:
raise ValueError(f"altcha verification failed: {verified[1]}")
return None
class SlopReport(BaseModel): class SlopReport(BaseModel):
"""Accept reports of one or more slop page URLs""" """Accept reports of one or more slop page URLs"""
slop_urls: Annotated[list[str], AfterValidator(url_validator)] slop_urls: Annotated[list[str], AfterValidator(url_validator)]
class SignupForm(BaseModel):
email: EmailStr
password: str
altcha_response: Annotated[Json, AfterValidator(altcha_validator)]

View File

@@ -8,18 +8,23 @@
- post report - post report
""" """
from typing import Annotated from typing import Annotated
from datetime import datetime, timedelta
import uvicorn import uvicorn
from fastapi import Depends, FastAPI, HTTPException from fastapi import Depends, FastAPI, Form, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine from sqlalchemy import create_engine
from pwdlib import PasswordHash from pwdlib import PasswordHash
from altcha import ChallengeOptions, create_challenge, verify_solution
from slopserver.models import Domain, Path, User from slopserver.models import Domain, Path, User
from slopserver.models import SlopReport from slopserver.models import SlopReport, SignupForm
from slopserver.db import select_slop, insert_slop, get_user from slopserver.db import select_slop, insert_slop, get_user, create_user
from slopserver.common import TEMP_HMAC_KEY
app = FastAPI() app = FastAPI()
@@ -30,6 +35,7 @@ TEMP_SECRET = "5bcc778a96b090c3ac1d587bb694a060eaf7bdb5832365f91d5078faf1fff210"
ALGO = "HS256" ALGO = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 ACCESS_TOKEN_EXPIRE_MINUTES = 30
password_hash = PasswordHash.recommended() password_hash = PasswordHash.recommended()
@@ -66,5 +72,26 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
if not user: if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password") raise HTTPException(status_code=400, detail="Incorrect username or password")
@app.post("/signup")
async def signup_form(form_data: Annotated[SignupForm, Form()]):
# if we're here, form is validated including the altcha
# check for existing user with the given email
if get_user(form_data.email, TEMP_ENGINE):
# user already exists
raise HTTPException(status_code=409, detail="User already exists")
# create user
create_user(form_data.email, get_password_hash(form_data.password), TEMP_ENGINE)
@app.get("/challenge")
async def altcha_challenge():
options = ChallengeOptions(
expires=datetime.now() + timedelta(minutes=10),
max_number=100000,
hmac_key=TEMP_HMAC_KEY
)
challenge = create_challenge(options)
return challenge
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)