Compare commits
3 Commits
1067b12be8
...
2a396457aa
Author | SHA1 | Date | |
---|---|---|---|
2a396457aa | |||
34c030c1e9 | |||
6eb2563068 |
8
db.py
8
db.py
@ -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)
|
||||
|
9
main.py
9
main.py
@ -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):
|
||||
|
@ -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",
|
||||
|
66
security.py
66
security.py
@ -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(
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { PlayerRanking } from "./types";
|
||||
import { useSession } from "./Session";
|
||||
|
||||
interface RaceChartProps {
|
||||
players: PlayerRanking[];
|
||||
|
@ -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 (
|
||||
|
@ -64,7 +64,6 @@ export function SessionProvider(props: SessionProviderProps) {
|
||||
setErr(e);
|
||||
}
|
||||
}
|
||||
console.log("sanity", user);
|
||||
|
||||
let content: ReactNode;
|
||||
if (loading || (!err && !user))
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user