Compare commits

..

No commits in common. "2a396457aabc1462dd9cd43b0a308a37cb3ace78" and "1067b12be89b52c2c874fc761447dc231342d162" have entirely different histories.

11 changed files with 90 additions and 155 deletions

8
db.py
View File

@ -73,12 +73,4 @@ class MVPRanking(SQLModel, table=True):
mvps: list[int] = Field(sa_column=Column(ARRAY(Integer))) mvps: list[int] = Field(sa_column=Column(ARRAY(Integer)))
class TokenDB(SQLModel, table=True):
token: str = Field(index=True, primary_key=True)
used: bool | None = False
updated_at: datetime | None = Field(
default_factory=utctime, sa_column_kwargs={"onupdate": utctime}
)
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

View File

@ -1,5 +1,4 @@
from fastapi import APIRouter, Depends, FastAPI, Security from fastapi import APIRouter, Depends, FastAPI, Security, status
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from db import Player, Team, Chemistry, MVPRanking, engine from db import Player, Team, Chemistry, MVPRanking, engine
from sqlmodel import ( from sqlmodel import (
@ -80,20 +79,18 @@ team_router.add_api_route("/list", endpoint=list_teams, methods=["GET"])
team_router.add_api_route("/add", endpoint=add_team, methods=["POST"]) team_router.add_api_route("/add", endpoint=add_team, methods=["POST"])
@api_router.post("/mvps", dependencies=[Depends(get_current_active_user)]) @app.post("/mvps/", status_code=status.HTTP_200_OK)
def submit_mvps(mvps: MVPRanking): def submit_mvps(mvps: MVPRanking):
with Session(engine) as session: with Session(engine) as session:
session.add(mvps) session.add(mvps)
session.commit() session.commit()
return JSONResponse("success!")
@api_router.post("/chemistry", dependencies=[Depends(get_current_active_user)]) @app.post("/chemistry/", status_code=status.HTTP_200_OK)
def submit_chemistry(chemistry: Chemistry): def submit_chemistry(chemistry: Chemistry):
with Session(engine) as session: with Session(engine) as session:
session.add(chemistry) session.add(chemistry)
session.commit() session.commit()
return JSONResponse("success!")
class SPAStaticFiles(StaticFiles): class SPAStaticFiles(StaticFiles):

View File

@ -19,7 +19,6 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@types/node": "^22.13.10",
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",

View File

@ -6,7 +6,7 @@ from pydantic import BaseModel, ValidationError
import jwt import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from sqlmodel import Session, select from sqlmodel import Session, select
from db import TokenDB, engine, Player from db import engine, Player
from fastapi.security import ( from fastapi.security import (
OAuth2PasswordBearer, OAuth2PasswordBearer,
OAuth2PasswordRequestForm, OAuth2PasswordRequestForm,
@ -178,7 +178,6 @@ async def login_for_access_token(
value=access_token, value=access_token,
httponly=True, httponly=True,
samesite="strict", samesite="strict",
max_age=config.access_token_expire_minutes * 60,
) )
return Token(access_token=access_token) return Token(access_token=access_token)
@ -205,17 +204,6 @@ class FirstPassword(BaseModel):
async def set_first_password(req: FirstPassword): async def set_first_password(req: FirstPassword):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate token",
)
with Session(engine) as session:
token_in_db = session.exec(
select(TokenDB)
.where(TokenDB.token == req.token)
.where(TokenDB.used == False)
).one_or_none()
if token_in_db:
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate token", detail="Could not validate token",
@ -235,25 +223,11 @@ async def set_first_password(req: FirstPassword):
user = get_user(username) user = get_user(username)
if user: if user:
with Session(engine) as session:
user.hashed_password = get_password_hash(req.password) user.hashed_password = get_password_hash(req.password)
session.add(user) session.add(user)
token_in_db.used = True
session.add(token_in_db)
session.commit() session.commit()
return Response( return Response("Password set successfully", status_code=status.HTTP_200_OK)
"Password set successfully", status_code=status.HTTP_200_OK
)
elif session.exec(
select(TokenDB)
.where(TokenDB.token == req.token)
.where(TokenDB.used == True)
).one_or_none():
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token already used",
)
else:
raise credentials_exception
async def change_password( async def change_password(

View File

@ -17,6 +17,13 @@ import { apiAuth } from "./api";
// }; // };
//}; //};
// //
interface Prop {
name: string;
min: string;
max: string;
step: string;
value: string;
}
interface Params { interface Params {
nodeSize: number; nodeSize: number;
@ -29,6 +36,12 @@ interface Params {
show: number; show: number;
} }
interface DeferredProps {
timeout: number;
func: () => void;
}
let timeoutID: NodeJS.Timeout | null = null; let timeoutID: NodeJS.Timeout | null = null;
export default function Analysis() { export default function Analysis() {
const [image, setImage] = useState(""); const [image, setImage] = useState("");
@ -52,10 +65,9 @@ export default function Analysis() {
.then((data) => { .then((data) => {
setImage(data.image); setImage(data.image);
setLoading(false); setLoading(false);
}) }).catch((e) => {
.catch((e) => {
console.log("best to just reload... ", e); console.log("best to just reload... ", e);
}); })
} }
useEffect(() => { useEffect(() => {
@ -69,35 +81,19 @@ export default function Analysis() {
function showLabel() { function showLabel() {
switch (params.show) { switch (params.show) {
case 0: case 0: return "dislike";
return "dislike"; case 1: return "both";
case 1: case 2: return "like";
return "both";
case 2:
return "like";
} }
} }
return ( return (
<div className="stack column dropdown"> <div className="stack column dropdown">
<button onClick={() => setShowControlPanel(!showControlPanel)}> <button onClick={() => setShowControlPanel(!showControlPanel)}>
Parameters{" "} Parameters <svg viewBox="0 0 24 24" height="1.2em" style={{ fill: "#ffffff", display: "inline", top: "0.2em", position: "relative", transform: showControlPanel ? "rotate(180deg)" : "unset" }} > <path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z" > </path></svg >
<svg
viewBox="0 0 24 24"
height="1.2em"
style={{
fill: "#ffffff",
display: "inline",
top: "0.2em",
position: "relative",
transform: showControlPanel ? "rotate(180deg)" : "unset",
}}
>
{" "}
<path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z"> </path>
</svg>
</button> </button>
<div id="control-panel" className={showControlPanel ? "opened" : ""}> <div id="control-panel" className={showControlPanel ? "opened" : ""}>
<div className="control"> <div className="control">
<datalist id="markers"> <datalist id="markers">
<option value="0"></option> <option value="0"></option>
@ -113,9 +109,7 @@ export default function Analysis() {
max="2" max="2"
step="1" step="1"
width="16px" width="16px"
onChange={(evt) => onChange={(evt) => setParams({ ...params, show: Number(evt.target.value) })}
setParams({ ...params, show: Number(evt.target.value) })
}
/> />
<label>😍</label> <label>😍</label>
</div> </div>
@ -126,9 +120,7 @@ export default function Analysis() {
<input <input
type="checkbox" type="checkbox"
checked={params.weighting} checked={params.weighting}
onChange={(evt) => onChange={(evt) => setParams({ ...params, weighting: evt.target.checked })}
setParams({ ...params, weighting: evt.target.checked })
}
/> />
<label>weighting</label> <label>weighting</label>
</div> </div>
@ -137,9 +129,7 @@ export default function Analysis() {
<input <input
type="checkbox" type="checkbox"
checked={params.popularity} checked={params.popularity}
onChange={(evt) => onChange={(evt) => setParams({ ...params, popularity: evt.target.checked })}
setParams({ ...params, popularity: evt.target.checked })
}
/> />
<label>popularity</label> <label>popularity</label>
</div> </div>
@ -153,12 +143,9 @@ export default function Analysis() {
max="3.001" max="3.001"
step="0.05" step="0.05"
value={params.distance} value={params.distance}
onChange={(evt) => onChange={(evt) => setParams({ ...params, distance: Number(evt.target.value) })}
setParams({ ...params, distance: Number(evt.target.value) })
}
/> />
<span>{params.distance}</span> <span>{params.distance}</span></div>
</div>
<div className="control"> <div className="control">
<label>node size</label> <label>node size</label>
@ -167,9 +154,7 @@ export default function Analysis() {
min="500" min="500"
max="3000" max="3000"
value={params.nodeSize} value={params.nodeSize}
onChange={(evt) => onChange={(evt) => setParams({ ...params, nodeSize: Number(evt.target.value) })}
setParams({ ...params, nodeSize: Number(evt.target.value) })
}
/> />
<span>{params.nodeSize}</span> <span>{params.nodeSize}</span>
</div> </div>
@ -181,9 +166,7 @@ export default function Analysis() {
min="4" min="4"
max="24" max="24"
value={params.fontSize} value={params.fontSize}
onChange={(evt) => onChange={(evt) => setParams({ ...params, fontSize: Number(evt.target.value) })}
setParams({ ...params, fontSize: Number(evt.target.value) })
}
/> />
<span>{params.fontSize}</span> <span>{params.fontSize}</span>
</div> </div>
@ -196,9 +179,7 @@ export default function Analysis() {
max="5" max="5"
step="0.1" step="0.1"
value={params.edgeWidth} value={params.edgeWidth}
onChange={(evt) => onChange={(evt) => setParams({ ...params, edgeWidth: Number(evt.target.value) })}
setParams({ ...params, edgeWidth: Number(evt.target.value) })
}
/> />
<span>{params.edgeWidth}</span> <span>{params.edgeWidth}</span>
</div> </div>
@ -210,19 +191,20 @@ export default function Analysis() {
min="10" min="10"
max="50" max="50"
value={params.arrowSize} value={params.arrowSize}
onChange={(evt) => onChange={(evt) => setParams({ ...params, arrowSize: Number(evt.target.value) })}
setParams({ ...params, arrowSize: Number(evt.target.value) })
}
/> />
<span>{params.arrowSize}</span> <span>{params.arrowSize}</span>
</div> </div>
</div> </div>
<button onClick={() => loadImage()}>reload </button> <button onClick={() => loadImage()}>reload </button>
{loading ? ( {
loading ? (
<span className="loader"></span> <span className="loader"></span>
) : ( ) : (
<img src={"data:image/png;base64," + image} width="86%" /> <img src={"data:image/png;base64," + image} width="86%" />
)} )
</div> }
</div >
); );
} }

View File

@ -1,8 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { apiAuth } from "./api"; import { apiAuth } from "./api";
import BarChart from "./BarChart";
import { PlayerRanking } from "./types"; import { PlayerRanking } from "./types";
import RaceChart from "./RaceChart"; import RaceChart from "./RaceChart";
const MVPChart = () => { const MVPChart = () => {
const [data, setData] = useState({} as PlayerRanking[]); const [data, setData] = useState({} as PlayerRanking[]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -11,26 +13,17 @@ const MVPChart = () => {
async function loadData() { async function loadData() {
setLoading(true); setLoading(true);
await apiAuth("analysis/mvp", null) await apiAuth("analysis/mvp", null)
.then((json) => json as Promise<PlayerRanking[]>) .then(json => json as Promise<PlayerRanking[]>).then(json => { setData(json.sort((a, b) => a.rank - b.rank)) })
.then((json) => {
setData(json.sort((a, b) => a.rank - b.rank));
});
setLoading(false); setLoading(false);
} }
useEffect(() => { useEffect(() => { loadData() }, [])
loadData();
}, []);
return ( return (
<> <>
{loading ? ( {loading ? <span className="loader" /> : <RaceChart std={showStd} players={data} />
<span className="loader" /> }
) : ( </>)
<RaceChart std={showStd} players={data} /> }
)}
</>
);
};
export default MVPChart; export default MVPChart;

View File

@ -1,5 +1,6 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { PlayerRanking } from "./types"; import { PlayerRanking } from "./types";
import { useSession } from "./Session";
interface RaceChartProps { interface RaceChartProps {
players: PlayerRanking[]; players: PlayerRanking[];

View File

@ -44,12 +44,12 @@ export function Chemistry({ user, players }: PlayerInfoProps) {
const dialog = document.querySelector("dialog[id='ChemistryDialog']"); const dialog = document.querySelector("dialog[id='ChemistryDialog']");
(dialog as HTMLDialogElement).showModal(); (dialog as HTMLDialogElement).showModal();
setDialog("sending..."); setDialog("sending...");
let left = playersLeft.map(({ id }) => id); let left = playersLeft.map(({ display_name }) => display_name);
let middle = playersMiddle.map(({ id }) => id); let middle = playersMiddle.map(({ display_name }) => display_name);
let right = playersRight.map(({ id }) => id); let right = playersRight.map(({ display_name }) => display_name);
const data = { user: user.id, hate: left, undecided: middle, love: right }; const data = { user: user, hate: left, undecided: middle, love: right };
const response = await apiAuth("chemistry", data, "POST"); const response = await apiAuth("chemistry", data);
response ? setDialog(response) : setDialog("try sending again"); response.ok ? setDialog("success!") : setDialog("try sending again");
} }
return ( return (
@ -124,10 +124,10 @@ export function MVP({ user, players }: PlayerInfoProps) {
const dialog = document.querySelector("dialog[id='MVPDialog']"); const dialog = document.querySelector("dialog[id='MVPDialog']");
(dialog as HTMLDialogElement).showModal(); (dialog as HTMLDialogElement).showModal();
setDialog("sending..."); setDialog("sending...");
let mvps = rankedPlayers.map(({ id }) => id); let mvps = rankedPlayers.map(({ display_name }) => display_name);
const data = { user: user.id, mvps: mvps }; const data = { user: user, mvps: mvps };
const response = await apiAuth("mvps", data, "POST"); const response = await apiAuth("mvps", data);
response ? setDialog(response) : setDialog("try sending again"); response.ok ? setDialog("success!") : setDialog("try sending again");
} }
return ( return (

View File

@ -64,6 +64,7 @@ export function SessionProvider(props: SessionProviderProps) {
setErr(e); setErr(e);
} }
} }
console.log("sanity", user);
let content: ReactNode; let content: ReactNode;
if (loading || (!err && !user)) if (loading || (!err && !user))

View File

@ -1,7 +1,7 @@
import { jwtDecode, JwtPayload } from "jwt-decode"; import { InvalidTokenError, jwtDecode, JwtPayload } from "jwt-decode";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { baseUrl } from "./api"; import { baseUrl } from "./api";
import { useNavigate } from "react-router"; import { redirect, useNavigate } from "react-router";
interface SetPassToken extends JwtPayload { interface SetPassToken extends JwtPayload {
name: string; name: string;
@ -63,9 +63,9 @@ export const SetPassword = () => {
if (!resp.ok) { if (!resp.ok) {
if (resp.status === 401) { if (resp.status === 401) {
const { detail } = await resp.json(); resp.statusText
if (detail) setError(detail); ? setError(resp.statusText)
else setError("unauthorized"); : setError("unauthorized");
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
} }

View File

@ -3,13 +3,10 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": [ "lib": ["ES2020", "DOM", "DOM.Iterable"],
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@ -17,6 +14,7 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": false, "noUnusedLocals": false,
@ -24,7 +22,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": [ "include": ["src"]
"src"
]
} }