7 Commits

Author SHA1 Message Date
dependabot[bot]
0465d0a1cd Bump cryptography from 46.0.3 to 46.0.5
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.3 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.3...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 02:41:14 +00:00
Jack Case
a36b6e9865 actually send the email 2025-11-15 16:04:45 +00:00
Jack Case
20fffc85c1 check if email is verified when authorizing 2025-11-15 15:53:02 +00:00
Jack Case
6724a903eb email verification responses 2025-11-15 15:45:05 +00:00
Jack Case
cfd54eca5e working on verification email sending 2025-11-15 14:04:03 +00:00
Jack Case
f5f8b5c873 change async path functions from async so that fastapi handles concurrency for non-async apis 2025-11-15 14:02:53 +00:00
Jack Case
f125fc1807 add resend to requirements and create email.py 2025-11-15 13:24:26 +00:00
5 changed files with 94 additions and 9 deletions

View File

@@ -7,7 +7,7 @@ argon2-cffi-bindings==25.1.0
certifi==2025.10.5
cffi==2.0.0
click==8.3.0
cryptography==46.0.3
cryptography==46.0.5
dnspython==2.8.0
email-validator==2.3.0
fastapi==0.119.0
@@ -35,6 +35,7 @@ PyJWT==2.10.1
python-dotenv==1.1.1
python-multipart==0.0.20
PyYAML==6.0.3
resend==2.19.0
rich==14.2.0
rich-toolkit==0.15.1
rignore==0.7.1

View File

@@ -71,7 +71,7 @@ def insert_slop(urls: list[ParseResult], engine: Engine, user: User | None = Non
session.commit()
def get_user(email, engine):
def get_user(email, engine) -> User:
query = select(User).where(User.email == email)
with Session(engine) as session:
@@ -84,3 +84,9 @@ def create_user(email, password_hash, engine):
with Session(engine) as session:
session.add(user)
session.commit()
def verify_user_email(user: User, engine):
with Session(engine) as session:
session.add(user)
user.email_verified = True
session.commit()

22
slopserver/email.py Normal file
View File

@@ -0,0 +1,22 @@
"""Send emails for account verification through Resend API"""
from slopserver.settings import settings
import resend
resend.api_key = settings.resend_token
def send_email(to: str, subject, content):
params: resend.Emails.SendParams = {
"from": "slopfarmer@jack-case.pro",
"to": to,
"subject": subject,
"html": content
}
email: resend.Emails.SendResponse = resend.Emails.send(params)
return email
def generate_verification_email(verification_url: str):
return f"""
<p>click here to verify your Slop Farmer account: <a href={verification_url}>{verification_url}</a></p>
"""

View File

@@ -15,6 +15,7 @@ import uvicorn
from fastapi import Body, Depends, FastAPI, Form, HTTPException, Header
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from pydantic import AfterValidator, Base64Str
@@ -32,7 +33,8 @@ from uuid import uuid4
from slopserver.settings import settings
from slopserver.models import Domain, Path, User
from slopserver.models import SlopReport, SignupForm, altcha_validator
from slopserver.db import select_slop, insert_slop, get_user, create_user
from slopserver.db import select_slop, insert_slop, get_user, create_user, verify_user_email
from slopserver.email import generate_verification_email, send_email
app = FastAPI()
@@ -87,6 +89,17 @@ def generate_auth_token(username):
encoded_jwt = jwt.encode(bearer_token, TOKEN_SECRET, ALGO)
return encoded_jwt
def generate_verification_token(username):
expiration = datetime.now() + timedelta(days=2)
verification_token = {
"iss": "slopserver",
"exp": int(expiration.timestamp()),
"sub": username
}
encoded_jwt = jwt.encode(verification_token, TOKEN_SECRET, ALGO)
return encoded_jwt
def get_token_user(decoded_token):
user = get_user(decoded_token["sub"], DB_ENGINE)
return user
@@ -98,13 +111,20 @@ def verify_auth_token(token: str):
except:
raise HTTPException(status_code=401, detail="invalid access token")
def verify_verification_token(token: str):
try:
token = jwt.decode(token, TOKEN_SECRET, ALGO)
return token
except:
raise HTTPException(status_code=404, detail="invalid verification URL")
@app.post("/report")
async def report_slop(report: SlopReport, bearer: Annotated[str, AfterValidator(verify_auth_token), Header()]):
def report_slop(report: SlopReport, bearer: Annotated[str, AfterValidator(verify_auth_token), Header()]):
user = get_token_user(bearer)
insert_slop(report.slop_urls, DB_ENGINE, user)
@app.post("/check")
async def check_slop(check: Annotated[SlopReport, Body()], bearer: Annotated[str, AfterValidator(verify_auth_token), Header()]):
def check_slop(check: Annotated[SlopReport, Body()], bearer: Annotated[str, AfterValidator(verify_auth_token), Header()]):
slop_results = select_slop(check.slop_urls, DB_ENGINE)
return slop_results
@@ -112,13 +132,13 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
pass
@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = get_user(form_data.username, DB_ENGINE)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
@app.post("/signup")
async def signup_form(form_data: Annotated[SignupForm, Form()]):
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, DB_ENGINE):
@@ -128,8 +148,38 @@ async def signup_form(form_data: Annotated[SignupForm, Form()]):
# create user
create_user(form_data.email, get_password_hash(form_data.password.get_secret_value()), DB_ENGINE)
# send verification email
# create a jwt encoding the username and a time limit to be the verification URL
token = generate_verification_token(form_data.email)
email_html = generate_verification_email(settings.api_base + "verify/?token=" + token)
status = send_email(form_data.email, "Slop Farmer Email Verification", email_html)
return status
@app.get("/verify")
def verify_email(token: Annotated[str, AfterValidator(verify_verification_token)]):
user = get_user(token["sub"], DB_ENGINE)
if not user:
raise HTTPException(status_code=404, detail="invalid verification URL")
if user.email_verified:
raise HTTPException(status_code=404, detail="already verified")
verify_user_email(user, DB_ENGINE)
html = f"""
<html>
<head>
</head>
<body>
<p>{token["sub"]} verified. You may log in now.</p>
</body>
</html>
"""
return HTMLResponse(content=html, status_code=200)
@app.get("/altcha-challenge")
async def altcha_challenge():
def altcha_challenge():
options = ChallengeOptions(
expires=datetime.now() + timedelta(minutes=10),
max_number=80000,
@@ -139,10 +189,12 @@ async def altcha_challenge():
return challenge
@app.post("/login")
async def simple_login(username: Annotated[str, Form()], password: Annotated[str, Form()]):
def simple_login(username: Annotated[str, Form()], password: Annotated[str, Form()]):
user = auth_user(username, password, DB_ENGINE)
if not user:
raise HTTPException(status_code=401, detail="Incorrect username or password")
if not user.email_verified:
raise HTTPException(status_code=401, detail="Unverified email address")
token = generate_auth_token(username)
return {"access_token": token, "token_type": "bearer"}

View File

@@ -5,5 +5,9 @@ class ServerSettings(BaseSettings):
db_url: str = "sqlite+pysqlite:///test_db.sqlite"
token_secret: str = "5bcc778a96b090c3ac1d587bb694a060eaf7bdb5832365f91d5078faf1fff210"
altcha_secret: str = "0460de065912d0292df1e7422a5ed2dc362ed56d6bab64fe50b89957463061f3"
resend_token: str = "re_NXpjzbqR_KgAbu72PKjYHcquX24WvnN3i"
sender_email: str = "slopfarmer@jack-case.pro"
api_base: str = "api.slopfarmer.jack-case.pro/"
settings = ServerSettings()