From b32b40db4905422c0809eafe156aac738ba2546d Mon Sep 17 00:00:00 2001 From: KittenTM Date: Fri, 6 Feb 2026 04:00:43 +0100 Subject: [PATCH] add limited db support in regards to the remember me button --- config.py | 13 ++++++++ database.py | 27 ++++++++++++++++ main.py | 15 ++++----- routes/sso.py | 83 ++++++++++++++++++++++++++++++++++++------------ services/auth.py | 13 +++++++- 5 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 config.py create mode 100644 database.py diff --git a/config.py b/config.py new file mode 100644 index 0000000..b439c27 --- /dev/null +++ b/config.py @@ -0,0 +1,13 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +from cryptography.fernet import Fernet + +class Settings(BaseSettings): + port: int = 5000 + db_url: str + fernet_key: str + cookie_httponly: bool = True + + model_config = SettingsConfigDict(env_file=".env", extra="ignore") + +settings = Settings() +cipher = Fernet(settings.fernet_key.encode()) \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..7c8e62e --- /dev/null +++ b/database.py @@ -0,0 +1,27 @@ +from sqlalchemy import create_engine, Column, String, Integer, Boolean, DateTime, ForeignKey, Text +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from config import settings +import uuid + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + id = Column(Integer, primary_key=True, index=True) + username = Column(String(16), unique=True, index=True, nullable=False) + local_hash = Column(Text, nullable=False) + spfn_pass_enc = Column(Text, nullable=False) + +class Session(Base): + __tablename__ = "sessions" + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + username = Column(String(16), ForeignKey("users.username"), nullable=False) + expires_at = Column(DateTime, nullable=True) + remember_me = Column(Boolean, default=False) + +engine = create_engine(settings.db_url) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def init_db(): + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/main.py b/main.py index 765dc84..4c39e8e 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,14 @@ from fastapi import FastAPI -from pydantic_settings import BaseSettings, SettingsConfigDict from routes import sso +from database import init_db +from config import settings import uvicorn -class Settings(BaseSettings): - port: int = 5000 - - model_config = SettingsConfigDict(env_file=".env", extra="ignore") - -settings = Settings() - app = FastAPI() + +init_db() + app.include_router(sso.router, prefix="/api/v2/sso") if __name__ == '__main__': - uvicorn.run(app, host="0.0.0.0", port=settings.port) \ No newline at end of file + uvicorn.run("main:app", host="0.0.0.0", port=settings.port, reload=True) \ No newline at end of file diff --git a/routes/sso.py b/routes/sso.py index e0a02b0..94f3ff9 100644 --- a/routes/sso.py +++ b/routes/sso.py @@ -1,36 +1,77 @@ -from fastapi import APIRouter, Request, Form, Response +from fastapi import APIRouter, Request, Form, Response, Depends from fastapi.responses import RedirectResponse -from typing import Optional +from sqlalchemy.orm import Session as DBSession +from database import SessionLocal, User, Session +from config import settings, cipher from services import auth +import uuid router = APIRouter() +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + @router.post('/spfn/generate_token') async def login( request: Request, - username: Optional[str] = Form(None), - password: Optional[str] = Form(None), - frontend_origin: Optional[str] = Form(None) + username: str = Form(...), + password: str = Form(...), + frontend_origin: str = Form(None), + rememberMe: bool = Form(False), + db: DBSession = Depends(get_db) ): host = (frontend_origin or request.headers.get("referer") or "/").rstrip('/') - sep = "&" if "?" in host else "?" - - if not username or not password: - return RedirectResponse(f"{host}/users/auth/splatfestival/{sep}error=auth&username={username or ''}", 303) - + try: - token = auth.get_token(username, password) - if not token: - return RedirectResponse(f"{host}/users/auth/splatfestival/{sep}error=auth&username={username}", 303) + print("fetching spfn token") + data = auth.get_token(username, password) + if not data or "token" not in data: + print("auth failed") + return RedirectResponse(f"{host}/login?error=auth", 303) - # Logging profile for debugging - profile = auth.get_profile(token) - if profile: - print(f"=== PROFILE ===\n{profile}\n===============") + print("encrypting password") + enc_pass = cipher.encrypt(password.encode()).decode() + + print(f"checking user {username}") + user = db.query(User).filter(User.username == username).first() + if not user: + print("creating new user") + user = User( + username=username, + local_hash=auth.hash_password(password), + spfn_pass_enc=enc_pass + ) + db.add(user) + else: + print("updating user") + user.spfn_pass_enc = enc_pass + + db.flush() + + print("creating session") + new_session = Session(username=username, remember_me=rememberMe) + db.add(new_session) + db.commit() - redirect_arg = request.query_params.get('redirect') - target = redirect_arg if redirect_arg else f"{host}/friend_list/" - return RedirectResponse(url=target, status_code=303) + print("setting cookie and redirecting") + response = RedirectResponse(url=f"{host}/friend_list/", status_code=303) + cookie_age = 2592000 if rememberMe else None + + response.set_cookie( + key="session_id", + value=new_session.id, + httponly=settings.cookie_httponly, + secure=True, + samesite="lax", + max_age=cookie_age + ) + return response except Exception as e: - return Response(content=f"Proxy Error: {str(e)}", status_code=500) \ No newline at end of file + print(f"error: {str(e)}") + db.rollback() + return Response(content=f"internal error: {str(e)}", status_code=500) \ No newline at end of file diff --git a/services/auth.py b/services/auth.py index f904387..d25218d 100644 --- a/services/auth.py +++ b/services/auth.py @@ -1,7 +1,18 @@ import requests import base64 +from argon2 import PasswordHasher API_URL = "https://account.spfn.net/api/v2" +ph = PasswordHasher() + +def hash_password(password: str): + return ph.hash(password) + +def verify_password(hashed: str, password: str): + try: + return ph.verify(hashed, password) + except Exception: + return False def get_token(username, password): creds = f"{username} {password}" @@ -13,7 +24,7 @@ def get_token(username, password): } response = requests.get(f"{API_URL}/oauth2/generate_token", headers=headers, timeout=10) - return response.json().get("token") if response.ok else None + return response.json() if response.ok else None def get_profile(token): headers = {