diff --git a/pom.xml b/pom.xml index 52baa01..2c0622e 100644 --- a/pom.xml +++ b/pom.xml @@ -133,6 +133,11 @@ io.quarkus quarkus-mailer + + + io.quarkus + quarkus-websockets-next + diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java index 77b4b94..6fd106c 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java @@ -59,6 +59,10 @@ public class CompetitionModel { String owner; + List admin = new ArrayList<>(); + @Column(name = "table_") + List table = new ArrayList<>(); + String data1; String data2; String data3; diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java index 5e3ba1e..23a7bd6 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java @@ -219,13 +219,58 @@ public class CompetPermService { if (o.getSystem() == CompetitionSystem.SAFCA) return hasSafcaEditPerm(securityCtx, o.getId()); - if (!securityCtx.isInClubGroup(o.getClub().getId())) // Only membre club pass here - throw new DForbiddenException(); - - if (o.getSystem() == CompetitionSystem.NONE) - if (securityCtx.isClubAdmin()) + if (o.getSystem() == CompetitionSystem.NONE) { + if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin()) return Uni.createFrom().nullItem(); + if (o.getAdmin().contains(securityCtx.getSubject())) + return Uni.createFrom().nullItem(); + + throw new DForbiddenException(); + } + + throw new DForbiddenException(); + }) + ); + } + + /** + * @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm + */ + public Uni hasTablePerm(SecurityCtx securityCtx, CompetitionModel competitionModel) { + return hasTablePerm(securityCtx, Uni.createFrom().item(competitionModel)); + } + + /** + * @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm + */ + public Uni hasTablePerm(SecurityCtx securityCtx, long id) { + return hasTablePerm(securityCtx, competitionRepository.findById(id)); + } + + /** + * @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm + */ + public Uni hasTablePerm(SecurityCtx securityCtx, Uni in) { + return in.call(Unchecked.function(o -> { + if (securityCtx.getSubject().equals(o.getOwner()) || securityCtx.roleHas("federation_admin")) + return Uni.createFrom().nullItem(); + + if (o.getSystem() == CompetitionSystem.SAFCA) + return hasSafcaTablePerm(securityCtx, o.getId()); + + if (o.getSystem() == CompetitionSystem.NONE) { + if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin()) + return Uni.createFrom().nullItem(); + + if (o.getAdmin().contains(securityCtx.getSubject())) + return Uni.createFrom().nullItem(); + if (o.getTable().contains(securityCtx.getSubject())) + return Uni.createFrom().nullItem(); + + throw new DForbiddenException(); + } + throw new DForbiddenException(); }) ); @@ -236,8 +281,8 @@ public class CompetPermService { Uni.createFrom().nullItem() : getSafcaConfig(id).chain(Unchecked.function(o -> { - if (!o.admin().contains(UUID.fromString(securityCtx.getSubject())) && !o.table() - .contains(UUID.fromString(securityCtx.getSubject()))) + if (!o.admin().contains(UUID.fromString(securityCtx.getSubject())) + && !o.table().contains(UUID.fromString(securityCtx.getSubject()))) throw new DForbiddenException(); return Uni.createFrom().nullItem(); })); @@ -253,4 +298,16 @@ public class CompetPermService { return Uni.createFrom().nullItem(); })); } + + private Uni hasSafcaTablePerm(SecurityCtx securityCtx, long id) { + return securityCtx.roleHas("safca_super_admin") ? + Uni.createFrom().nullItem() + : + getSafcaConfig(id).chain(Unchecked.function(o -> { + if (!o.admin().contains(UUID.fromString(securityCtx.getSubject())) + && !o.table().contains(UUID.fromString(securityCtx.getSubject()))) + throw new DForbiddenException(); + return Uni.createFrom().nullItem(); + })); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index f8cb994..ecd48a4 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -56,7 +56,7 @@ public class ResultService { public Uni> getCategory(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) .chain(m -> categoryRepository.list("compet.uuid = ?1", uuid) - .chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1) OR category IN ?2", //TODO AND + .chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True m.getMembre(), cats))) .map(matchModels -> { HashMap> map = new HashMap<>(); diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java new file mode 100644 index 0000000..5de5908 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -0,0 +1,176 @@ +package fr.titionfire.ffsaf.ws; + +import com.fasterxml.jackson.core.JsonProcessingException; +import fr.titionfire.ffsaf.data.repository.CompetitionRepository; +import fr.titionfire.ffsaf.domain.service.CompetPermService; +import fr.titionfire.ffsaf.net2.MessageType; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import fr.titionfire.ffsaf.ws.data.WelcomeInfo; +import fr.titionfire.ffsaf.ws.recv.RMatch; +import fr.titionfire.ffsaf.ws.recv.WSReceiver; +import fr.titionfire.ffsaf.ws.send.JsonUni; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.security.Authenticated; +import io.quarkus.websockets.next.*; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.ForbiddenException; +import org.jboss.logging.Logger; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER; + +@Authenticated +@WebSocket(path = "api/ws/competition/{uuid}") +public class CompetitionWS { + private static final Logger LOGGER = Logger.getLogger(CompetitionWS.class); + + private static final HashMap>> waitingResponse = new HashMap<>(); + + @Inject + RMatch rMatch; + + @Inject + SecurityCtx securityCtx; + + @Inject + CompetPermService competPermService; + + @Inject + CompetitionRepository competitionRepository; + + HashMap wsMethods = new HashMap<>(); + + public void getWSReceiverMethods(Class clazz, Object object) { + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(WSReceiver.class)) { + if (method.getParameterCount() <= 1) { + LOGGER.warnf("@WSReceiver has no parameters for method %s", method.getName()); + continue; + } + if (!method.getReturnType().equals(Uni.class)) { + LOGGER.warnf("@WSReceiver has returned unexpected type %s", method.getReturnType()); + continue; + } + // method.setAccessible(true); + wsMethods.put(method, object); + } + } + } + + @PostConstruct + void init() { + getWSReceiverMethods(RMatch.class, rMatch); + } + + @OnOpen + @WithSession + Uni open(WebSocketConnection connection) { + LOGGER.infof("Opening CompetitionWS for %s", connection.pathParam("uuid")); + LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size()); + + return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult() + .invoke(Unchecked.consumer(cm -> { + if (cm == null) + throw new ForbiddenException(); + })) + .call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> "admin") + .onFailure() + .recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> "table")) + .onFailure() + .recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> "view")) + .invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem)) + .invoke(prem -> LOGGER.infof("Connection permission: %s", prem)) + .onFailure().transform(t -> new ForbiddenException())) + .invoke(__ -> { + connection.userData().put(UserData.TypedKey.forString("subject"), securityCtx.getSubject()); + waitingResponse.put(connection, new HashMap<>()); + }) + .map(cm -> { + WelcomeInfo welcomeInfo = new WelcomeInfo(); + + welcomeInfo.setName(cm.getName()); + welcomeInfo.setPerm(connection.userData().get(UserData.TypedKey.forString("prem"))); + + return new MessageOut(UUID.randomUUID(), "welcomeInfo", MessageType.NOTIFY, welcomeInfo); + }); + } + + @OnClose + void close(WebSocketConnection connection) { + LOGGER.infof("Closing CompetitionWS for %s ", connection.pathParam("uuid")); + LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size()); + + waitingResponse.remove(connection); + } + + private MessageOut makeReply(MessageIn message, Object data) { + return new MessageOut(message.uuid(), message.code(), MessageType.REPLY, data); + } + + private MessageOut makeError(MessageIn message, Object data) { + return new MessageOut(message.uuid(), message.code(), MessageType.ERROR, data); + } + + @OnTextMessage + Multi processAsync(WebSocketConnection connection, MessageIn message) { + + if (message.type() == MessageType.REPLY || message.type() == MessageType.ERROR) { + try { + JsonUni jsonUni = waitingResponse.get(connection).get(message.uuid()); + if (jsonUni == null) { + LOGGER.debugf("No JsonUni found for %s", message.uuid()); + if (message.type() == MessageType.ERROR) + LOGGER.errorf("request %s make error %s", message.uuid(), message.data()); + return null; + } + waitingResponse.get(connection).remove(message.uuid()); + + if (message.type() == MessageType.ERROR) + return jsonUni.castAndError(message.data()).onFailure() + .invoke(t -> LOGGER.error(t.getMessage(), t)) + .replaceWith((MessageOut) null).toMulti().filter(__ -> false); + + return jsonUni.castAndChain(message.data()).onFailure() + .invoke(t -> LOGGER.error(t.getMessage(), t)) + .replaceWith((MessageOut) null).toMulti().filter(__ -> false); + } catch (JsonProcessingException e) { + LOGGER.warn(e.getMessage(), e); + } + } + + try { + for (Map.Entry entry : wsMethods.entrySet()) { + Method method = entry.getKey(); + if (method.getAnnotation(WSReceiver.class).code().equalsIgnoreCase(message.code())) { + return ((Uni) method.invoke(entry.getValue(), connection, + MAPPER.treeToValue(message.data(), method.getParameterTypes()[1]))) + .map(o -> makeReply(message, o)) + .onFailure() + .recoverWithItem(t -> makeError(message, t.getMessage())).toMulti() + .filter(__ -> message.type() == MessageType.REQUEST); + } + } + return Uni.createFrom().item(makeError(message, "No receiver method found")).toMulti(); + } catch (IllegalAccessException | InvocationTargetException | JsonProcessingException e) { + LOGGER.warn(e.getMessage(), e); + return Uni.createFrom().item(makeError(message, e.getMessage())).toMulti(); + } + + // return Uni.createFrom().item(new Message<>(message.uuid(), message.code(), MessageType.REPLY, "ko")); + } + + @OnError + Uni error(WebSocketConnection connection, ForbiddenException t) { + return connection.close(CloseReason.INTERNAL_SERVER_ERROR); + //return "forbidden: " + securityCtx.getSubject(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/MessageIn.java b/src/main/java/fr/titionfire/ffsaf/ws/MessageIn.java new file mode 100644 index 0000000..99532ec --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/MessageIn.java @@ -0,0 +1,12 @@ +package fr.titionfire.ffsaf.ws; + +import com.fasterxml.jackson.databind.JsonNode; +import fr.titionfire.ffsaf.net2.MessageType; +import io.quarkus.runtime.annotations.RegisterForReflection; + +import java.util.UUID; + +@RegisterForReflection +public record MessageIn (UUID uuid, String code, MessageType type, JsonNode data){ + +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/MessageOut.java b/src/main/java/fr/titionfire/ffsaf/ws/MessageOut.java new file mode 100644 index 0000000..4db5c3e --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/MessageOut.java @@ -0,0 +1,11 @@ +package fr.titionfire.ffsaf.ws; + +import fr.titionfire.ffsaf.net2.MessageType; +import io.quarkus.runtime.annotations.RegisterForReflection; + +import java.util.UUID; + +@RegisterForReflection +public record MessageOut(UUID uuid, String code, MessageType type, Object data){ + +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java b/src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java new file mode 100644 index 0000000..eebe679 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java @@ -0,0 +1,11 @@ +package fr.titionfire.ffsaf.ws.data; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Data; + +@Data +@RegisterForReflection +public class WelcomeInfo { + private String name; + private String perm; +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java new file mode 100644 index 0000000..fcb0afa --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java @@ -0,0 +1,29 @@ +package fr.titionfire.ffsaf.ws.recv; + +import fr.titionfire.ffsaf.data.repository.MatchRepository; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.logging.Logger; + +@WithSession +@ApplicationScoped +@RegisterForReflection +public class RMatch { + private static final Logger LOGGER = Logger.getLogger(RMatch.class); + + @Inject + MatchRepository matchRepository; + + @WSReceiver(code = "getAllMatch") + public Uni getAllMatch(WebSocketConnection connection, Long l) { + LOGGER.info("getAllMatch " + l); + + return Uni.createFrom().item(l); + //return matchRepository.count(); + } + +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java new file mode 100644 index 0000000..183b455 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java @@ -0,0 +1,14 @@ +package fr.titionfire.ffsaf.ws.recv; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@RegisterForReflection +@Retention(RetentionPolicy.RUNTIME) +public @interface WSReceiver { + + String code(); + +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/JsonUni.java b/src/main/java/fr/titionfire/ffsaf/ws/send/JsonUni.java new file mode 100644 index 0000000..abe91ac --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/JsonUni.java @@ -0,0 +1,33 @@ +package fr.titionfire.ffsaf.ws.send; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; + +import java.util.function.Function; + +import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER; + +@RegisterForReflection +public class JsonUni { + + private final Class clazz; + private final Function> mapper; + + public JsonUni(Class clazz, Function> mapper) { + this.clazz = clazz; + this.mapper = mapper; + } + + public Uni castAndChain(JsonNode message) throws JsonProcessingException { + return Uni.createFrom().item(MAPPER.treeToValue(message, clazz)).chain(mapper); + } + + public Uni castAndError(JsonNode message) throws JsonProcessingException { + return Uni.createFrom().item((T) null).invoke(Unchecked.consumer(__ -> { + throw new WSClientError(MAPPER.treeToValue(message, String.class)); + })).chain(mapper); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/WSClientError.java b/src/main/java/fr/titionfire/ffsaf/ws/send/WSClientError.java new file mode 100644 index 0000000..b654569 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/WSClientError.java @@ -0,0 +1,13 @@ +package fr.titionfire.ffsaf.ws.send; + +import java.io.IOException; +import java.io.Serial; + +public class WSClientError extends IOException { + @Serial + private static final long serialVersionUID = -3790479241838684450L; + + public WSClientError(String message) { + super(message); + } +} diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index 16c3872..433d889 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -1,4 +1,4 @@ -import {useEffect, useRef} from 'react' +import {lazy, Suspense, useEffect, useRef} from 'react' import {Nav} from "./components/Nav.jsx"; import {createBrowserRouter, Outlet, RouterProvider, useLocation, useRouteError} from "react-router-dom"; import {Home} from "./pages/Homepage.jsx"; @@ -59,6 +59,11 @@ const router = createBrowserRouter([ element: , children: getResultChildren() }, + { + path: 'competition-manager/*', + element: , + //children: getCompetitionManagerChildren() + }, { path: 'me', element: @@ -71,6 +76,17 @@ const router = createBrowserRouter([ } ]) +function GetCompetitionMangerLazy() { + const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx')) + return +

Compétition manager

+

Chargement...

+ }> + +
+} + function PageError() { const error = useRouteError() return
diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index 1230802..0410cb4 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -55,6 +55,7 @@ function CompMenu() {
  • Inscription
  • Mes résultats
  • +
  • Compétitions Manager
} diff --git a/src/main/webapp/src/hooks/useWS.jsx b/src/main/webapp/src/hooks/useWS.jsx new file mode 100644 index 0000000..e475704 --- /dev/null +++ b/src/main/webapp/src/hooks/useWS.jsx @@ -0,0 +1,154 @@ +import {createContext, useContext, useEffect, useReducer, useRef, useState} from "react"; + +function uuidv4() { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => + (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) + ); +} + +const WebsocketContext = createContext([false, () => { +}]) + +function reducer(state, action) { + switch (action.type) { + case 'addListener': + return { + ...state, + listener: [...state.listener, {callback: action.payload.callback, code: action.payload.code}] + } + case 'removeListener': + return { + ...state, + listener: state.listener.filter(l => l.callback !== action.payload) + } + case 'clearListeners': + return { + ...state, + listener: state.listener.filter(l => l.code !== action.payload) + } + case 'clearAllListeners': + return { + ...state, + listener: [] + } + default: + return state + } +} + + +export function WSProvider({url, onmessage, children}) { + const [isReady, setIsReady] = useState(false) + const [state, dispatch] = useReducer(reducer, {listener: []}) + const ws = useRef(null) + const listenersRef = useRef([]) + const callbackRef = useRef({}) + + useEffect(() => { + listenersRef.current = state.listener + }, [state.listener]) + + useEffect(() => { + console.log("WSProvider: connecting to", url); + const socket = new WebSocket(url) + + socket.onopen = () => setIsReady(true) + socket.onclose = () => setIsReady(false) + socket.onmessage = (event) => { + const msg = JSON.parse(event.data) + + if (msg.type === "REPLY" || msg.type === "ERROR") { + const cb = callbackRef.current[msg.uuid]; + if (cb) { + if (msg.type === "REPLY") + cb.onReply(msg.data); + else + cb.onError(msg.data); + delete callbackRef.current[msg.uuid] + } + return; + } + + let isHandled = false; + + listenersRef.current + .filter(l => l.code === msg.code) + .forEach(l => { + try { + l.callback({...msg}) + isHandled = true; + } catch (err) { + console.error("Listener callback error:", err) + } + }); + if (!isHandled && onmessage) + onmessage(JSON.parse(event.data)) + } + + ws.current = socket + return () => { + socket.close() + } + }, []) + + const send = (uuid, code, type, data, onReply = () => { + }, onError = () => { + }) => { + if (!isReady) { + onError("WebSocket is not connected"); + return; + } + if (type === "REQUEST") { + callbackRef.current[uuid] = {onReply: onReply, onError: onError} + } + ws.current?.send(JSON.stringify({ + uuid: uuid, + code: code, + type: type, + data: data + })) + } + + + const ret = {isReady, dispatch, send, wait_length: callbackRef} + return + {children} + +} + +export function useWS() { + const {isReady, dispatch, send, wait_length} = useContext(WebsocketContext) + return { + dispatch, + isReady, + wait_length, + sendRequest: (code, data) => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject("timeout"); + }, 1000); + + send(uuidv4(), code, "REQUEST", data, (data) => { + clearTimeout(timeout); + resolve(data); + }, (data) => { + clearTimeout(timeout); + reject(data); + }); + }) + }, + sendNotify: (code, data) => { + send(uuidv4(), code, "NOTIFY", data) + }, + sendReply: (message, data) => { + send(message.uuid, message.code, "REPLY", data) + }, + sendReplyError: (message, data) => { + send(message.uuid, message.code, "ERROR", data) + }, + sendError: (data) => { + send(uuidv4(), "error", "ERROR", data) + }, + send, + } +} diff --git a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx new file mode 100644 index 0000000..1b2f7d4 --- /dev/null +++ b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx @@ -0,0 +1,111 @@ +import {Route, Routes, useNavigate, useParams} from "react-router-dom"; +import {LoadingProvider} from "../../../hooks/useLoading.jsx"; +import {useEffect, useState} from "react"; +import {useWS, WSProvider} from "../../../hooks/useWS.jsx"; +import {ColoredCircle} from "../../../components/ColoredCircle.jsx"; + +const vite_url = import.meta.env.VITE_URL; + +export default function CompetitionManagerRoot() { + return <> +

Compétition manager

+ + + }/> + }/> + + + +} + +function Home() { + const nav = useNavigate(); + return
+

Home

+ +
+} + +function HomeComp() { + let {compUuid} = useParams(); + const [perm, setPerm] = useState("") + + const messageHandler = msg => { + console.log("Received WS message:", msg); + } + + return + + + }/> + }/> + + +} + +function WSStatus({setPerm}) { + const [name, setName] = useState("") + const [inWait, setInWait] = useState(false) + const {isReady, wait_length, dispatch} = useWS(); + + useEffect(() => { + const timer = setInterval(() => { + setInWait(Object.keys(wait_length.current).length > 0); + }, 250); + return () => clearInterval(timer); + }, []); + + useEffect(() => { + const welcomeListener = ({data}) => { + setName(data.name) + setPerm(data.perm) + } + dispatch({type: 'addListener', payload: {callback: welcomeListener, code: 'welcomeInfo'}}) + return () => dispatch({type: 'removeListener', payload: welcomeListener}) + }, []) + + return
+

{name}

+
Serveur: +
+
+} + +function Home2({perm}) { + const nav = useNavigate(); + const {sendRequest} = useWS(); + + return
+

Sélectionne les modes d'affichage

+
+ {perm === "admin" && <> + + + } + {perm === "table" && <> + + } +
+ + + +
+} + +function Test2() { + let {compUuid} = useParams(); + const nav = useNavigate(); + return
+

Product ID: {compUuid}

+ +
+} diff --git a/src/main/webapp/vite.config.js b/src/main/webapp/vite.config.js index 6b5b768..7233a9d 100644 --- a/src/main/webapp/vite.config.js +++ b/src/main/webapp/vite.config.js @@ -16,6 +16,7 @@ export default ({mode}) => { "/api": { target: process.env.VITE_API_URL, changeOrigin: true, + ws: true, }, "/q": { target: process.env.VITE_API_URL,