diff --git a/analysis.py b/analysis.py index 9f0d88e..ea1ed7e 100644 --- a/analysis.py +++ b/analysis.py @@ -1,130 +1,20 @@ -from datetime import timedelta, timezone, datetime +from datetime import datetime import io -from typing import Annotated import base64 -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter from fastapi.responses import JSONResponse from pydantic import BaseModel, Field -import jwt -from jwt.exceptions import InvalidTokenError from sqlmodel import Session, func, select from sqlmodel.sql.expression import SelectOfScalar -from db import Chemistry, Player, engine, User +from db import Chemistry, Player, engine import networkx as nx -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from pydantic_settings import BaseSettings, SettingsConfigDict -from passlib.context import CryptContext import matplotlib matplotlib.use("agg") import matplotlib.pyplot as plt -class Config(BaseSettings): - secret_key: str = "" - access_token_expire_minutes: int = 30 - model_config = SettingsConfigDict( - env_file=".env", env_file_encoding="utf-8", extra="ignore" - ) - - -config = Config() - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: str | None = None - - -pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") - - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -analysis_router = APIRouter(prefix="/analysis", dependencies=[Depends(oauth2_scheme)]) - - -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return pwd_context.hash(password) - - -def get_user(username: str): - with Session(engine) as session: - return session.exec(select(User).where(User.username == username)).one_or_none() - - -def authenticate_user(username: str, password: str): - user = get_user(username) - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: timedelta | None = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta - else: - expire = datetime.now(timezone.utc) + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, config.secret_key, algorithm="HS256") - return encoded_jwt - - -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - payload = jwt.decode(token, config.secret_key, algorithms=["HS256"]) - username: str = payload.get("sub") - if username is None: - raise credentials_exception - token_data = TokenData(username=username) - except InvalidTokenError: - raise credentials_exception - user = get_user(fake_users_db, username=token_data.username) - if user is None: - raise credentials_exception - return user - - -async def get_current_active_user( - current_user: Annotated[User, Depends(get_current_user)], -): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -@analysis_router.post("/token") -async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], -) -> Token: - user = authenticate_user(form_data.username, form_data.password) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - access_token_expires = timedelta(minutes=config.access_token_expire_minutes) - access_token = create_access_token( - data={"sub": user.username}, expires_delta=access_token_expires - ) - return Token(access_token=access_token, token_type="bearer") +analysis_router = APIRouter(prefix="/analysis") C = Chemistry diff --git a/main.py b/main.py index 301239f..3ab6c20 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, FastAPI, status +from fastapi import APIRouter, Depends, FastAPI, status from fastapi.staticfiles import StaticFiles from db import Player, Team, Chemistry, MVPRanking, engine from sqlmodel import ( @@ -7,7 +7,12 @@ from sqlmodel import ( ) from fastapi.middleware.cors import CORSMiddleware from analysis import analysis_router -from security import login_for_access_token, read_users_me, read_own_items +from security import ( + get_current_active_user, + login_for_access_token, + read_users_me, + read_own_items, +) app = FastAPI(title="cutt") @@ -92,7 +97,9 @@ class SPAStaticFiles(StaticFiles): api_router.include_router(player_router) api_router.include_router(team_router) -api_router.include_router(analysis_router) +api_router.include_router( + analysis_router, dependencies=[Depends(get_current_active_user)] +) api_router.add_api_route("/token", endpoint=login_for_access_token, methods=["POST"]) api_router.add_api_route("/users/me/", endpoint=read_users_me, methods=["GET"]) api_router.add_api_route("/users/me/items/", endpoint=read_own_items, methods=["GET"]) diff --git a/package.json b/package.json index 9b9d86e..c41dfec 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.14.0", - "react-router-dom": "^7.1.5", + "react-router": "^7.1.5", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", "vite": "^6.0.5" diff --git a/security.py b/security.py index df250a1..71e1d84 100644 --- a/security.py +++ b/security.py @@ -1,6 +1,6 @@ from datetime import timedelta, timezone, datetime from typing import Annotated -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, Response, status from pydantic import BaseModel import jwt from jwt.exceptions import InvalidTokenError @@ -34,7 +34,7 @@ class TokenData(BaseModel): pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/token") def verify_password(plain_password, hashed_password): @@ -102,7 +102,7 @@ async def get_current_active_user( async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], response: Response ) -> Token: user = authenticate_user(form_data.username, form_data.password) if not user: @@ -115,6 +115,9 @@ async def login_for_access_token( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) + response.set_cookie( + "Authorization", value=f"Bearer {access_token}", httponly=True, samesite="none" + ) return Token(access_token=access_token, token_type="bearer") diff --git a/src/Analysis.tsx b/src/Analysis.tsx index a60a6b3..e1b2b75 100644 --- a/src/Analysis.tsx +++ b/src/Analysis.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { baseUrl } from "./api"; +import { apiAuth } from "./api"; //const debounce = void>( // func: T, @@ -61,18 +61,13 @@ export default function Analysis() { // Function to generate and fetch the graph image async function loadImage() { setLoading(true); - await fetch(`${baseUrl}api/analysis/image`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(params) - }) - .then((resp) => resp.json()) + await apiAuth("analysis/image", params, "POST") .then((data) => { setImage(data.image); setLoading(false); - }); + }).catch((e) => { + console.log("best to just reload... ", e); + }) } useEffect(() => { diff --git a/src/App.css b/src/App.css index 99fb625..7ae39f7 100644 --- a/src/App.css +++ b/src/App.css @@ -12,16 +12,18 @@ body { height: 100%; } -footer { - font-size: x-small; -} - #root { max-width: 1280px; margin: 0 auto; padding: 8px; } +footer { + margin-top: 24px; + font-size: x-small; +} + + .grey { color: #444; } @@ -37,6 +39,11 @@ footer { z-index: -1; } +input { + padding: 0.2em 16px; + margin-bottom: 0.5em; +} + h1, h2, h3 { @@ -120,7 +127,8 @@ h3 { margin: auto; } -button { +button, +.button { font-weight: bold; font-size: large; color: aliceblue; @@ -201,6 +209,10 @@ button { } .navbar { + span { + padding: 4px; + } + button { font-size: medium; margin: 4px 0.5%; diff --git a/src/App.tsx b/src/App.tsx index 41ab79f..870c388 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,8 @@ import "./App.css"; import Footer from "./Footer"; import Header from "./Header"; import Rankings from "./Rankings"; -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Routes, Route } from "react-router"; +import { SessionProvider } from "./Session"; function App() { //const [data, setData] = useState({ nodes: [], links: [] } as SociogramData); @@ -17,7 +18,11 @@ function App() {
} /> - } /> + + + + } />