Compare commits
8 Commits
v0.8
...
feat_testi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cb9ae8606 | ||
|
|
6a1837762e | ||
|
|
a36b6e9865 | ||
|
|
20fffc85c1 | ||
|
|
6724a903eb | ||
|
|
cfd54eca5e | ||
|
|
f5f8b5c873 | ||
|
|
f125fc1807 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -214,4 +214,7 @@ __marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
|
||||
slopserver/server_config.env
|
||||
test_db.sqlite
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
22
slopserver/email.py
Normal 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>
|
||||
"""
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@ from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ServerSettings(BaseSettings):
|
||||
db_url: str = "sqlite+pysqlite:///test_db.sqlite"
|
||||
db_url: str = "sqlite+pysqlite:///slopserver/test/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()
|
||||
|
||||
0
slopserver/test/__init__.py
Normal file
0
slopserver/test/__init__.py
Normal file
5
slopserver/test/backup_test_db.sh
Executable file
5
slopserver/test/backup_test_db.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
test_dir=slopserver/test
|
||||
|
||||
sqlite3 $test_dir/test_db.sqlite .dump > $1
|
||||
5
slopserver/test/create_test_db.sh
Executable file
5
slopserver/test/create_test_db.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
test_dir=slopserver/test
|
||||
|
||||
cat $test_dir/test_db.sql | sqlite3 $test_dir/test_db.sqlite
|
||||
5
slopserver/test/test_api.py
Normal file
5
slopserver/test/test_api.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from slopserver.server import app as slopserver_app
|
||||
|
||||
client = TestClient(slopserver_app)
|
||||
15
slopserver/test/test_db.py
Normal file
15
slopserver/test/test_db.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from slopserver.db import *
|
||||
from slopserver.models import *
|
||||
from slopserver.settings import settings
|
||||
from sqlalchemy import create_engine
|
||||
import unittest
|
||||
|
||||
class TestDBFuncs(unittest.TestCase):
|
||||
|
||||
test_db_url = settings.db_url
|
||||
engine = None
|
||||
|
||||
def setUp(self):
|
||||
engine = create_engine(self.test_db_url)
|
||||
|
||||
|
||||
45
slopserver/test/test_db.sql
Normal file
45
slopserver/test/test_db.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE domain (
|
||||
id INTEGER NOT NULL,
|
||||
domain_name VARCHAR NOT NULL,
|
||||
CONSTRAINT pk_domain PRIMARY KEY (id)
|
||||
);
|
||||
INSERT INTO domain VALUES(1,'google.com');
|
||||
INSERT INTO domain VALUES(2,'moogle.com');
|
||||
CREATE TABLE user (
|
||||
id INTEGER NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
password_hash VARCHAR NOT NULL,
|
||||
email_verified BOOLEAN NOT NULL,
|
||||
CONSTRAINT pk_user PRIMARY KEY (id)
|
||||
);
|
||||
INSERT INTO user VALUES(1,'alphauser01','$argon2id$v=19$m=65536,t=3,p=4$3z7uxa3NHl/dKG07RGEvBA$0NOBftJpP+HiR7wfgdwBk2UR9F12YBjrqeqLSyDl47o','True');
|
||||
CREATE TABLE path (
|
||||
id INTEGER NOT NULL,
|
||||
path VARCHAR NOT NULL,
|
||||
domain_id INTEGER,
|
||||
CONSTRAINT pk_path PRIMARY KEY (id),
|
||||
CONSTRAINT fk_path_domain_id_domain FOREIGN KEY(domain_id) REFERENCES domain (id)
|
||||
);
|
||||
INSERT INTO path VALUES(1,'/',0);
|
||||
INSERT INTO path VALUES(2,'/path1',0);
|
||||
INSERT INTO path VALUES(3,'/path2',1);
|
||||
INSERT INTO path VALUES(4,'/path3/a',1);
|
||||
INSERT INTO path VALUES(5,'/path4',1);
|
||||
INSERT INTO path VALUES(6,'/path2',2);
|
||||
INSERT INTO path VALUES(7,'/',2);
|
||||
INSERT INTO path VALUES(8,'/path3',2);
|
||||
INSERT INTO path VALUES(9,'/path4',2);
|
||||
CREATE TABLE report (
|
||||
path_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
timestamp DATETIME,
|
||||
CONSTRAINT pk_report PRIMARY KEY (path_id, user_id),
|
||||
CONSTRAINT fk_report_path_id_path FOREIGN KEY(path_id) REFERENCES path (id),
|
||||
CONSTRAINT fk_report_user_id_user FOREIGN KEY(user_id) REFERENCES user (id)
|
||||
);
|
||||
INSERT INTO report VALUES(2,1,'11-26-2025');
|
||||
CREATE UNIQUE INDEX ix_domain_domain_name ON domain (domain_name);
|
||||
CREATE UNIQUE INDEX ix_user_email ON user (email);
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user