myheats

Live heats, scoring and leaderboard for sport events
git clone https://git.in0rdr.ch/myheats.git
Log | Files | Refs | Pull requests | README | LICENSE

commit 68031fcdd01b7366a33bc527670a68ce12781fb7
parent c7e44fcae0065fb38b68fc3faae5d8c6e7789de8
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Sun, 15 Sep 2024 22:34:52 +0200

feat(magic): add AuthVerify comp & redo session

Diffstat:
Msrc/App.jsx | 29+++++++++++------------------
Msrc/Athletes.jsx | 2+-
Msrc/Auth.jsx | 25++++++++++++++++---------
Asrc/AuthVerify.jsx | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/Heats.jsx | 2+-
Msrc/Score.jsx | 8++++----
Msrc/Startlist.jsx | 2+-
7 files changed, 79 insertions(+), 34 deletions(-)

diff --git a/src/App.jsx b/src/App.jsx @@ -2,18 +2,21 @@ import './App.css' import { Suspense, lazy, useState, useEffect, Fragment } from 'react' import { BrowserRouter as Router, Routes, Route, Outlet, Link, NavLink } from 'react-router-dom' import { supabase } from './supabaseClient' - +import { CookiesProvider, useCookies } from 'react-cookie' const Score = lazy(() => import('./Score')) const Heats = lazy(() => import('./Heats')) const Athletes = lazy(() => import('./Athletes')) const Startlist = lazy(() => import('./Startlist')) const Auth = lazy(() => import('./Auth')) +const AuthVerify = lazy(() => import('./AuthVerify')) const Leaderboard = lazy(() => import('./Leaderboard')) document.title = import.meta.env.VITE_APP_DOC_TITLE ? import.meta.env.VITE_APP_DOC_TITLE : 'My Heats' -function Layout({session}) { +function Layout() { + const [session, setSession, destroySession] = useCookies(['auth']) + return ( <Fragment> <nav> @@ -25,21 +28,21 @@ function Layout({session}) { Leaderboard </NavLink> </li> - {session ? <li> + {session.auth ? <li> <NavLink className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} to="/score"> Scoring </NavLink> </li> : ''} - {session ? <li> + {session.auth ? <li> <NavLink className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} to="/heats"> Heats and Startlists </NavLink> </li> : ''} - {session ? <li> + {session.auth ? <li> <NavLink className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} to="/athletes"> @@ -55,7 +58,7 @@ function Layout({session}) { <br /> <span className='version'>MyHeats <a href="https://code.in0rdr.ch/myheats/refs.html">v0.5-nightly</a></span> <span className='login'> - {session ? <button onClick={() => supabase.auth.signOut()}> Sign out {session.user.email} </button> : + {session.auth ? <button onClick={() => destroySession('auth')}>Sign out {session.auth.email}</button> : <NavLink className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} to="/auth"> @@ -77,18 +80,7 @@ function NoMatch() { } function App() { - const [session, setSession] = useState(null) - - useEffect(() => { - // Returns the session, refreshing it if necessary - supabase.auth.getSession().then(({ data: { session } }) => { - setSession(session) - }) - - supabase.auth.onAuthStateChange((_event, session) => { - setSession(session) - }) - }, []) + const [session] = useCookies(['auth']) return ( <Fragment> @@ -102,6 +94,7 @@ function App() { <Route path="/athletes" element={<Athletes session={session} />} /> <Route path="/startlist/:heatId" element={<Startlist session={session} />} /> <Route path="/auth" element={<Auth />} /> + <Route path="/authverify" element={<AuthVerify />} /> <Route path="*" element={<NoMatch />} /> </Route> </Routes> diff --git a/src/Athletes.jsx b/src/Athletes.jsx @@ -129,7 +129,7 @@ function AthleteForm({session}) { function Athletes({session}) { return ( <div> - {!session ? <Auth /> : <AthleteForm session={session} />} + {!session.auth ? <Auth /> : <AthleteForm session={session} />} </div> ) } diff --git a/src/Auth.jsx b/src/Auth.jsx @@ -1,5 +1,7 @@ import { useState } from 'react' -import { supabase } from './supabaseClient' + +const api_uri = import.meta.env.MYHEATS_API ? import.meta.env.MYHEATS_API: 'http://127.0.0.1' +const api_port = import.meta.env.MYHEATS_API_PORT ? import.meta.env.MYHEATS_API_PORT: '8000' function Auth() { const [loading, setLoading] = useState(false) @@ -10,14 +12,19 @@ function Auth() { try { setLoading(true) - const { error } = await supabase.auth.signInWithOtp({ - email: email, - options: { - shouldCreateUser: false, - emailRedirectTo: window.location.origin - }}) - if (error) throw error - alert('Check your email for the login link!') + + const response = await fetch(`${api_uri}:${api_port}/v1/auth/requestMagicLink`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({"email": email}), + }) + + const { data, error } = await response.json() + if (error) { + alert(error) + } else { + alert('Check your email for the login link!') + } } catch (error) { alert(error.error_description || error.message) } finally { diff --git a/src/AuthVerify.jsx b/src/AuthVerify.jsx @@ -0,0 +1,45 @@ +import { useState, useEffect } from 'react' +import { CookiesProvider, useCookies } from 'react-cookie' + +const api_uri = import.meta.env.MYHEATS_API ? import.meta.env.MYHEATS_API: 'http://127.0.0.1' +const api_port = import.meta.env.MYHEATS_API_PORT ? import.meta.env.MYHEATS_API_PORT: '8000' + +function AuthVerify() { + const [loading, setLoading] = useState(true) + const [session, setSession, removeSession] = useCookies(['user']) + const [response, setResponse] = useState({}) + const queryParameters = new URLSearchParams(window.location.search) + const token = queryParameters.get("token") + + useEffect(() => { + (async () => { + // check token validity on API endpoint + const response = await fetch(`${api_uri}:${api_port}/v1/auth/verify?token=${token}`) + const { message, data, error } = await response.json() + setResponse(data) + console.log("Uncle roger be like:", message) + + if (error) { + alert(error) + } else { + // Set client session (https://www.npmjs.com/package/react-cookie) + setSession('auth', data, { + path: '/', + secure: process.env.NODE_ENV !== 'development', + // todo: reuses Token expiration, this could be different + expires: new Date(data.expires_at), + }) + + // redirect if everything ok + window.location.replace('/'); + } + })(); + }, []); + + return ( + <div className="AuthVerify"> + </div> + ) +} + +export default AuthVerify; diff --git a/src/Heats.jsx b/src/Heats.jsx @@ -119,7 +119,7 @@ function HeatForm({session}) { function Heats({session}) { return ( <div> - {!session ? <Auth /> : <HeatForm session={session} />} + {!session.auth ? <Auth /> : <HeatForm session={session} />} </div> ) } diff --git a/src/Score.jsx b/src/Score.jsx @@ -59,7 +59,7 @@ function ScoringForm({session}) { const currentScore = await supabase.from('scores').select() .eq('heat', heatSelection.value) .eq('athlete', athleteSelection.value) - .eq('judge', session.user.id) + .eq('judge', session.id) if (score === 0 && currentScore.data?.length > 0) { // fallback to current score when no new scoring took place @@ -69,11 +69,11 @@ function ScoringForm({session}) { updateScore(score, heatSelection.value, athleteSelection.value, - session.user.id) + session.id) } setLoading(false) })(); - }, [heatSelection, athleteSelection, session.user.id, score]); + }, [heatSelection, athleteSelection, session.id, score]); return ( <div> @@ -118,7 +118,7 @@ function ScoringForm({session}) { function Score({session}) { return ( <div> - {!session ? <Auth /> : <ScoringForm session={session} />} + {!session.auth ? <Auth /> : <ScoringForm session={session} />} </div> ) } diff --git a/src/Startlist.jsx b/src/Startlist.jsx @@ -150,7 +150,7 @@ function Startlist({session}) { return ( <div> - {!session ? <Auth /> : <StartlistForm heatId={heatId} />} + {!session.auth ? <Auth /> : <StartlistForm heatId={heatId} />} </div> ) }