Compare commits

...

3 Commits

Author SHA1 Message Date
2a396457aa
chore: remove unused stuff 2025-03-11 15:43:36 +01:00
34c030c1e9
feat: adjust submission function to new DB 2025-03-11 13:52:54 +01:00
6eb2563068
feat: implement one-time token 2025-03-11 13:37:23 +01:00
11 changed files with 155 additions and 90 deletions

8
db.py
View File

@ -73,4 +73,12 @@ class MVPRanking(SQLModel, table=True):
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)

View File

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

View File

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

View File

@ -6,7 +6,7 @@ from pydantic import BaseModel, ValidationError
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from sqlmodel import Session, select
from db import engine, Player
from db import TokenDB, engine, Player
from fastapi.security import (
OAuth2PasswordBearer,
OAuth2PasswordRequestForm,
@ -178,6 +178,7 @@ async def login_for_access_token(
value=access_token,
httponly=True,
samesite="strict",
max_age=config.access_token_expire_minutes * 60,
)
return Token(access_token=access_token)
@ -208,26 +209,51 @@ async def set_first_password(req: FirstPassword):
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate token",
)
try:
payload = jwt.decode(req.token, config.secret_key, algorithms=["HS256"])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Access token expired",
)
except (InvalidTokenError, ValidationError):
raise credentials_exception
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(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate token",
)
try:
payload = jwt.decode(req.token, config.secret_key, algorithms=["HS256"])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Access token expired",
)
except (InvalidTokenError, ValidationError):
raise credentials_exception
user = get_user(username)
if user:
with Session(engine) as session:
user.hashed_password = get_password_hash(req.password)
session.add(user)
session.commit()
return Response("Password set successfully", status_code=status.HTTP_200_OK)
user = get_user(username)
if user:
user.hashed_password = get_password_hash(req.password)
session.add(user)
token_in_db.used = True
session.add(token_in_db)
session.commit()
return Response(
"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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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