commit 4ec3f79cf92408497e2288e94544dbe3a97a365e
parent 5b54a388e20db1f7b21739b279068e1d8236a920
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date: Sat, 28 Sep 2024 14:23:46 +0200
feat: authenticate heats, athletes, startlists
Diffstat:
6 files changed, 97 insertions(+), 56 deletions(-)
diff --git a/src/api/server.cjs b/src/api/server.cjs
@@ -103,7 +103,7 @@ server.on('request', async (req, res) => {
// cors pre-flight request uses options method
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
// extact authorization token
const authHeader = req.headers.authorization
@@ -374,7 +374,8 @@ server.on('request', async (req, res) => {
);
if (scores.length < 1) {
- throw new Error("Score not found")
+ noContent(res);
+ return
}
res.end(JSON.stringify({
diff --git a/src/frontend/Heats.jsx b/src/frontend/Heats.jsx
@@ -8,11 +8,14 @@ const locale = import.meta.env.VITE_LOCALE
const Auth = lazy(() => import('./Auth'))
-export async function addNewHeat(name, heatLocation, plannedStart) {
+export async function addNewHeat(name, heatLocation, plannedStart, session) {
try {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/newHeat`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"name": name,
"location": heatLocation,
@@ -26,7 +29,7 @@ export async function addNewHeat(name, heatLocation, plannedStart) {
}
}
-async function addHeat(e) {
+async function addHeat(e, session) {
e.preventDefault()
// Read the form data
@@ -40,7 +43,8 @@ async function addHeat(e) {
formJson.location,
// planned_start is an empty string if unset
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time
- formJson.planned_start === '' ? null : formJson.planned_start
+ formJson.planned_start === '' ? null : formJson.planned_start,
+ session
)
window.location.reload()
} catch (error) {
@@ -48,10 +52,13 @@ async function addHeat(e) {
}
}
-export async function removeHeat(heatId) {
+export async function removeHeat(heatId, session) {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/removeHeat`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"heat_id": heatId
}),
@@ -59,11 +66,11 @@ export async function removeHeat(heatId) {
return await res.json()
}
-async function deleteHeat(e, heatId, heatName) {
+async function deleteHeat(e, heatId, heatName, session) {
e.preventDefault()
if (window.confirm('Do you really want to delete heat "' + heatName + '"?')) {
- const { data, error } = await removeHeat(heatId)
+ const { data, error } = await removeHeat(heatId, session)
if (error === undefined) {
window.location.reload()
} else {
@@ -108,7 +115,7 @@ function HeatForm({session}) {
return (
<div>
<button disabled={!loading}>{loading ? '↺ loading' : ''}</button>
- <form method='post' onSubmit={addHeat}>
+ <form method='post' onSubmit={e => addHeat(e, session)}>
<table>
<thead>
<tr>
@@ -126,7 +133,7 @@ function HeatForm({session}) {
<td data-title='Name'><Link to={generatePath('/startlist/:heatId', {heatId:h.id})}>{h.name}</Link></td>
<td data-title='Location'>{h.location}</td>
<td data-title='Planned start' className='right'>{h.planned_start}</td>
- <td><button onClick={e => deleteHeat(e, h.id, h.name)}>– del</button></td>
+ <td><button onClick={e => deleteHeat(e, h.id, h.name, session)}>– del</button></td>
</tr>
))}
<tr className='input'>
diff --git a/src/frontend/Leaderboard.jsx b/src/frontend/Leaderboard.jsx
@@ -1,4 +1,4 @@
-import { exportLeaderboardToCSV, rankByHeat, getScores } from './utils'
+import { exportLeaderboardToCSV, rankByHeat, formatScores } from './utils'
import { Fragment, useEffect, useState, useRef } from 'react'
import Select from 'react-select'
import { addNewHeat } from './Heats'
@@ -23,10 +23,13 @@ socket.onopen = function (event) {
}))
}
-export async function addAthleteToHeat(athlete, heat) {
+export async function addAthleteToHeat(athlete, heat, session) {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/addAthleteToHeat`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"athlete": athlete,
"heat": heat,
@@ -120,8 +123,8 @@ async function getScoreSummary(heatIds) {
}
// find best/worst heat, use 'n/a' if no score yet
- i.bestHeat = i.heats.length > 0 ? Math.max(...i.heats.map(h => h.summary)) : 'n/a'
- i.worstHeat = i.heats.length > 0 ? Math.min(...i.heats.map(h => h.summary)) : 'n/a'
+ i.bestHeat = i.heats.length > 0 ? Math.max(...i.heats.map(h => h.summary.toFixed(1))) : 'n/a'
+ i.worstHeat = i.heats.length > 0 ? Math.min(...i.heats.map(h => h.summary.toFixed(1))) : 'n/a'
// sum up all totals across heats
i.sum = i.heats.map(h => h.summary).reduce((a, b) => a + b, 0).toFixed(1)
@@ -133,7 +136,7 @@ async function getScoreSummary(heatIds) {
return startListWithScores
}
-async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRef, selectRankRef}) {
+async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRef, selectRankRef, session}) {
e.preventDefault()
if (leaderboard.length === 0) {
@@ -151,7 +154,8 @@ async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRe
heat = await addNewHeat(
formJson.name,
formJson.location,
- formJson.planned_start === '' ? null : formJson.planned_start
+ formJson.planned_start === '' ? null : formJson.planned_start,
+ session
)
} catch (error) {
console.error(error)
@@ -161,7 +165,7 @@ async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRe
for (let i = 0; i < formJson.size && i < sortedBoard.length; i++ ) {
// add top N athletes from current leaderboard to new heat
try {
- await addAthleteToHeat(sortedBoard[i].athlete, heat.id)
+ await addAthleteToHeat(sortedBoard[i].athlete, heat.id, session)
} catch (error) {
console.error(error)
}
@@ -191,7 +195,7 @@ function ExportForm({leaderboard, heatSelection, rankingComp}) {
)
}
-function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef) {
+function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef, session) {
return (
<div className='newHeatForm'>
<h2>New Heat from top N</h2>
@@ -203,7 +207,8 @@ function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef) {
leaderboard,
rankingComp,
selectHeatRef,
- selectRankRef
+ selectRankRef,
+ session,
)}>
<table>
<thead>
@@ -398,7 +403,7 @@ function Leaderboard({session}) {
{heatSelection.map(h => (
<Fragment key={h.value}>
{/* list all scores from the judges seperated with '+' signs, show sum on right side */}
- <td className='right' data-title={h.label}>{getScores(i, h)} = {i.heats.find(heat => heat.heatId === h.value)?.summary}</td>
+ <td className='right' data-title={h.label}>{formatScores(i, h)}</td>
</Fragment>
))}
<td className='right' data-title='Best'>{i.bestHeat}</td>
@@ -418,6 +423,7 @@ function Leaderboard({session}) {
rankingComp={rankingComp}
selectHeatRef={selectHeatRef}
selectRankRef={selectRankRef}
+ session={session}
/> : ''}
</div>
)
diff --git a/src/frontend/Score.jsx b/src/frontend/Score.jsx
@@ -6,14 +6,17 @@ import Select from 'react-select'
const api_uri = import.meta.env.VITE_API_URI
const api_port = import.meta.env.VITE_API_PORT
-async function getScore(heatId, athleteId, userId) {
+async function getScore(heatId, athleteId, session) {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/getScore`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"heat": heatId,
"athlete": athleteId,
- "judge": userId,
+ "judge": session.auth.id,
}),
})
const { data, error } = await res.json()
@@ -23,15 +26,18 @@ async function getScore(heatId, athleteId, userId) {
return data
}
-async function updateScore(score, heatId, athleteId, userId) {
+async function updateScore(score, heatId, athleteId, session) {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/setScore`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"score": score,
"heat": heatId,
"athlete": athleteId,
- "judge": userId,
+ "judge": session.auth.id,
}),
})
const { data, error } = await res.json()
@@ -93,7 +99,7 @@ function ScoringForm({session}) {
const currentScore = await getScore(
heatSelection.value,
athleteSelection.value,
- session.auth.id
+ session
)
if (score === 0) {
// fallback to current score when no new scoring took place
@@ -108,7 +114,7 @@ function ScoringForm({session}) {
await updateScore(score,
heatSelection.value,
athleteSelection.value,
- session.auth.id)
+ session)
} catch (error) {
console.error(error)
}
diff --git a/src/frontend/Startlist.jsx b/src/frontend/Startlist.jsx
@@ -7,24 +7,27 @@ import Select from 'react-select'
const api_uri = import.meta.env.VITE_API_URI
const api_port = import.meta.env.VITE_API_PORT
-async function addtoHeat(e, athlete, heatId) {
+async function addtoHeat(e, athlete, heatId, session) {
e.preventDefault()
try {
- await addAthleteToHeat(athlete.value, heatId)
+ await addAthleteToHeat(athlete.value, heatId, session)
window.location.reload()
} catch(error) {
console.error(error)
}
}
-async function removeAthleteFromHeat(e, startlistId, athleteFirstName, athleteLastName, heatName) {
+async function removeAthleteFromHeat(e, startlistId, athleteFirstName, athleteLastName, heatName, session) {
e.preventDefault()
const athleteName = athleteFirstName + (athleteLastName ? ' ' + athleteLastName : '')
if (window.confirm('Do you really want to remove athlete "' + athleteName + '" from heat "' + heatName + '"?')) {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/removeAthleteFromHeat`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"startlist_id": startlistId,
}),
@@ -37,10 +40,13 @@ async function removeAthleteFromHeat(e, startlistId, athleteFirstName, athleteLa
}
}
-async function startlistWithAthletes(heatId) {
+async function startlistWithAthletes(heatId, session) {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/startlistWithAthletes`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"heat_id": heatId,
}),
@@ -52,10 +58,13 @@ async function startlistWithAthletes(heatId) {
return data
}
-async function getHeat(heatId) {
+async function getHeat(heatId, session) {
const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/getHeat`, {
method: 'POST',
- headers: {'Content-Type': 'application/json'},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
body: JSON.stringify({
"heat_id": heatId,
}),
@@ -67,7 +76,7 @@ async function getHeat(heatId) {
return data
}
-function StartlistForm({heatId}) {
+function StartlistForm({heatId, session}) {
const [heatName, setheatName] = useState("")
const [heatLocation, setheatLocation] = useState("")
const [heatStart, setheatStart] = useState("")
@@ -82,10 +91,10 @@ function StartlistForm({heatId}) {
setLoading(true)
try {
- const startlist = await startlistWithAthletes(heatId)
+ const startlist = await startlistWithAthletes(heatId, session)
setStartlist(startlist)
- const heat = await getHeat(heatId)
+ const heat = await getHeat(heatId, session)
setheatName(heat.name)
setheatLocation(heat.location)
setheatStart(heat.planned_start)
@@ -93,7 +102,12 @@ function StartlistForm({heatId}) {
console.error(error)
}
- const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/allAthletes`)
+ const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/allAthletes`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.auth.token}`,
+ },
+ })
const { data, error } = await res.json()
if (error) {
console.error(error)
@@ -144,7 +158,8 @@ function StartlistForm({heatId}) {
i.startlist_id,
i.firstname,
i.lastname,
- heatName
+ heatName,
+ session
)}>– del</button></td>
</tr>
))}
@@ -156,7 +171,7 @@ function StartlistForm({heatId}) {
/>
</td>
<td>
- <button onClick={(e) => addtoHeat(e, selectedAthlete, heatId)}>+ add</button>
+ <button onClick={(e) => addtoHeat(e, selectedAthlete, heatId, session)}>+ add</button>
</td>
</tr>
</tbody>
@@ -170,7 +185,7 @@ function Startlist({session}) {
return (
<div>
- {!session.auth ? <Auth /> : <StartlistForm heatId={heatId} />}
+ {!session.auth ? <Auth /> : <StartlistForm heatId={heatId} session={session} />}
</div>
)
}
diff --git a/src/frontend/utils.js b/src/frontend/utils.js
@@ -42,15 +42,10 @@ export const exportLeaderboardToCSV = async function(e, leaderboard, heatSelecti
// concatenate heat labels
const heatNames = heatSelection.map(h => h.label)
- // rank leaderboard by selected comparator
+ // rank leaderboard by selected comparator for each entry i in the board,
+ // for every heat h
const heatSummaries = leaderboard.sort(rankByHeat(rankingComp)).map(i =>
- // for each entry i in the board, for every heat h
- heatSelection.map(h =>
- // get individual scores of the heat
- getScores(i, h) + " = "
- // get heat score sum
- + i.heats.find(heat => heat.heatId === h.value)?.summary
- )
+ heatSelection.map(h => formatScores(i, h))
)
// csv header
@@ -102,8 +97,19 @@ export const rankByHeat = function(rankingComp) {
// Scores concat with "+" for leaderboard entry i and heat h
export const getScores = function(i, h) {
- const scores = i.heats.find(heat => heat.heatId === h.value)?.scores?.map(s => s.score).join(" + ")
- return scores === "" ? 0 : scores
+ const scores = i.heats.find(heat => heat.heatId === h.value)?.scores?.map(s => s.score.toFixed(1)).join(" + ")
+ return scores
+}
+// Returns formatted string with summed scores and summary for leaderboard
+// entry i and heat h
+export const formatScores = function(i, h) {
+ const scores = getScores(i, h)
+ if (scores) {
+ // get individual scores of the heat and score sum
+ return getScores(i, h) + " = " + i.heats.find(heat => heat.heatId === h.value)?.summary.toFixed(1)
+ } else {
+ return "n/a"
+ }
}
// export CSV as blob