From b9beb112681077400cfb46d2984d8d778ac68ce5 Mon Sep 17 00:00:00 2001 From: KittenTM Date: Fri, 6 Feb 2026 23:06:27 +0100 Subject: [PATCH] implement autologin on login page --- config.py | 5 +++-- example_dot.env | 5 ++++- main.py | 25 ++++++++++++++++++---- routes/sso.py | 56 +++++++++++++++++++++++++++++++++---------------- routes/user.py | 33 +++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 routes/user.py diff --git a/config.py b/config.py index b439c27..aae3550 100644 --- a/config.py +++ b/config.py @@ -5,9 +5,10 @@ class Settings(BaseSettings): port: int = 5000 db_url: str fernet_key: str - cookie_httponly: bool = True - + cookie_httponly: bool = False + frontend_url: str model_config = SettingsConfigDict(env_file=".env", extra="ignore") + cookie_secure: bool = False settings = Settings() cipher = Fernet(settings.fernet_key.encode()) \ No newline at end of file diff --git a/example_dot.env b/example_dot.env index ebbd51f..1ce2bc7 100644 --- a/example_dot.env +++ b/example_dot.env @@ -1 +1,4 @@ -APP_PORT=5000 \ No newline at end of file +FERNET_KEY= +DB_URL= +PORT=5000 +FRONTEND_URL= \ No newline at end of file diff --git a/main.py b/main.py index 4c39e8e..a5c5088 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,31 @@ -from fastapi import FastAPI -from routes import sso -from database import init_db +from fastapi import FastAPI, Request +from routes import sso, user from config import settings import uvicorn +from fastapi.middleware.cors import CORSMiddleware app = FastAPI() -init_db() +app.add_middleware( + CORSMiddleware, + allow_origins=[settings.frontend_url], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["Set-Cookie"], +) + +@app.middleware("http") +async def force_cors_on_errors(request: Request, call_next): + response = await call_next(request) + origin = request.headers.get("origin") + if origin == settings.frontend_url: + response.headers["Access-Control-Allow-Origin"] = origin + response.headers["Access-Control-Allow-Credentials"] = "true" + return response app.include_router(sso.router, prefix="/api/v2/sso") +app.include_router(user.router, prefix="/api/v1/users") if __name__ == '__main__': 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 94f3ff9..de1f426 100644 --- a/routes/sso.py +++ b/routes/sso.py @@ -4,6 +4,7 @@ from sqlalchemy.orm import Session as DBSession from database import SessionLocal, User, Session from config import settings, cipher from services import auth +from typing import Optional import uuid router = APIRouter() @@ -18,56 +19,75 @@ def get_db(): @router.post('/spfn/generate_token') async def login( request: Request, - username: str = Form(...), - password: str = Form(...), - frontend_origin: str = Form(None), + username: Optional[str] = Form(None), + password: Optional[str] = Form(None), + frontend_origin: Optional[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 "?" + auth_path = f"{host}/users/auth/splatfestival/" + print(f"host: {host}") + + if not username or not password: + print("blank fields detected") + return RedirectResponse(f"{auth_path}{sep}error=auth&username={username or ''}", 303) + try: 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) + return RedirectResponse(f"{auth_path}{sep}error=auth&username={username}", 303) - 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") + print("creating new user with argon2id") + enc_pass = cipher.encrypt(password.encode()).decode() if rememberMe else "" user = User( username=username, local_hash=auth.hash_password(password), spfn_pass_enc=enc_pass ) db.add(user) + elif rememberMe: + print("updating encrypted pass for client") + user.spfn_pass_enc = cipher.encrypt(password.encode()).decode() else: - print("updating user") - user.spfn_pass_enc = enc_pass + print("client requested no password storage, skipping update") db.flush() + + print("checking for existing session") + active_session = db.query(Session).filter(Session.username == username).first() - print("creating session") - new_session = Session(username=username, remember_me=rememberMe) - db.add(new_session) + if active_session: + print(f"refreshing session for {username}") + active_session.remember_me = rememberMe + else: + print(f"creating new session for {username}") + active_session = Session(username=username, remember_me=rememberMe) + db.add(active_session) + db.commit() - print("setting cookie and redirecting") + print("setting client-based cookie") response = RedirectResponse(url=f"{host}/friend_list/", status_code=303) cookie_age = 2592000 if rememberMe else None + #you are a pain in the ass respectfully response.set_cookie( key="session_id", - value=new_session.id, + value=active_session.id, httponly=settings.cookie_httponly, - secure=True, + secure=False, samesite="lax", - max_age=cookie_age + path="/", + max_age=cookie_age, + domain=None ) return response diff --git a/routes/user.py b/routes/user.py new file mode 100644 index 0000000..a522cbc --- /dev/null +++ b/routes/user.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Request, Depends, HTTPException +from sqlalchemy.orm import Session as DBSession +from database import SessionLocal, User, Session +from typing import Dict + +router = APIRouter() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@router.get('/me') +async def get_current_user(request: Request, db: DBSession = Depends(get_db)): + session_id = request.cookies.get("session_id") + if not session_id: + raise HTTPException(status_code=401, detail="No session cookie") + active_session = db.query(Session).filter(Session.id == session_id).first() + if not active_session: + print(f"invalid or expired session: {session_id}") + raise HTTPException(status_code=401, detail="invalid session") + user = db.query(User).filter(User.username == active_session.username).first() + + if not user: + print(f"session linked to non-existent user: {active_session.username}") + raise HTTPException(status_code=401, detail="user not found") + print(f"auto-login success for {user.username}") + return { + "status": "ok", + "username": user.username + } \ No newline at end of file