wip: implement ws base
This commit is contained in:
parent
22fa896ee0
commit
a83088387b
5
pom.xml
5
pom.xml
@ -133,6 +133,11 @@
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-mailer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets-next</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@ -59,6 +59,10 @@ public class CompetitionModel {
|
||||
|
||||
String owner;
|
||||
|
||||
List<String> admin = new ArrayList<>();
|
||||
@Column(name = "table_")
|
||||
List<String> table = new ArrayList<>();
|
||||
|
||||
String data1;
|
||||
String data2;
|
||||
String data3;
|
||||
|
||||
@ -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<CompetitionModel> 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<CompetitionModel> 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<CompetitionModel> hasTablePerm(SecurityCtx securityCtx, Uni<CompetitionModel> 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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ public class ResultService {
|
||||
public Uni<List<ResultCategoryData>> 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<Long, List<MatchModel>> map = new HashMap<>();
|
||||
|
||||
176
src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java
Normal file
176
src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java
Normal file
@ -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<WebSocketConnection, HashMap<UUID, JsonUni<?>>> waitingResponse = new HashMap<>();
|
||||
|
||||
@Inject
|
||||
RMatch rMatch;
|
||||
|
||||
@Inject
|
||||
SecurityCtx securityCtx;
|
||||
|
||||
@Inject
|
||||
CompetPermService competPermService;
|
||||
|
||||
@Inject
|
||||
CompetitionRepository competitionRepository;
|
||||
|
||||
HashMap<Method, Object> 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<MessageOut> 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<MessageOut> 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<Method, Object> 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<Void> error(WebSocketConnection connection, ForbiddenException t) {
|
||||
return connection.close(CloseReason.INTERNAL_SERVER_ERROR);
|
||||
//return "forbidden: " + securityCtx.getSubject();
|
||||
}
|
||||
}
|
||||
12
src/main/java/fr/titionfire/ffsaf/ws/MessageIn.java
Normal file
12
src/main/java/fr/titionfire/ffsaf/ws/MessageIn.java
Normal file
@ -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){
|
||||
|
||||
}
|
||||
11
src/main/java/fr/titionfire/ffsaf/ws/MessageOut.java
Normal file
11
src/main/java/fr/titionfire/ffsaf/ws/MessageOut.java
Normal file
@ -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){
|
||||
|
||||
}
|
||||
11
src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java
Normal file
11
src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java
Normal file
@ -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;
|
||||
}
|
||||
29
src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java
Normal file
29
src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java
Normal file
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
14
src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java
Normal file
14
src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java
Normal file
@ -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();
|
||||
|
||||
}
|
||||
33
src/main/java/fr/titionfire/ffsaf/ws/send/JsonUni.java
Normal file
33
src/main/java/fr/titionfire/ffsaf/ws/send/JsonUni.java
Normal file
@ -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<T> {
|
||||
|
||||
private final Class<T> clazz;
|
||||
private final Function<? super T, Uni<?>> mapper;
|
||||
|
||||
public JsonUni(Class<T> clazz, Function<? super T, Uni<?>> 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);
|
||||
}
|
||||
}
|
||||
13
src/main/java/fr/titionfire/ffsaf/ws/send/WSClientError.java
Normal file
13
src/main/java/fr/titionfire/ffsaf/ws/send/WSClientError.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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: <ResultRoot/>,
|
||||
children: getResultChildren()
|
||||
},
|
||||
{
|
||||
path: 'competition-manager/*',
|
||||
element: <GetCompetitionMangerLazy/>,
|
||||
//children: getCompetitionManagerChildren()
|
||||
},
|
||||
{
|
||||
path: 'me',
|
||||
element: <MePage/>
|
||||
@ -71,6 +76,17 @@ const router = createBrowserRouter([
|
||||
}
|
||||
])
|
||||
|
||||
function GetCompetitionMangerLazy() {
|
||||
const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx'))
|
||||
return <Suspense
|
||||
fallback={<div>
|
||||
<h1>Compétition manager</h1>
|
||||
<p>Chargement...</p>
|
||||
</div>}>
|
||||
<CMLazy/>
|
||||
</Suspense>
|
||||
}
|
||||
|
||||
function PageError() {
|
||||
const error = useRouteError()
|
||||
return <div style={{textAlign: 'center'}}>
|
||||
|
||||
@ -55,6 +55,7 @@ function CompMenu() {
|
||||
<ul className="dropdown-menu">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions Manager</NavLink></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
154
src/main/webapp/src/hooks/useWS.jsx
Normal file
154
src/main/webapp/src/hooks/useWS.jsx
Normal file
@ -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 <WebsocketContext.Provider value={ret}>
|
||||
{children}
|
||||
</WebsocketContext.Provider>
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -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 <>
|
||||
<h1>Compétition manager</h1>
|
||||
<LoadingProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home/>}/>
|
||||
<Route path="/:compUuid/*" element={<HomeComp/>}/>
|
||||
</Routes>
|
||||
</LoadingProvider>
|
||||
</>
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const nav = useNavigate();
|
||||
return <div>
|
||||
<h2>Home</h2>
|
||||
<button onClick={() => nav("d3dc76a6-2058-423a-b34b-6d15d7ae5848")}>Go comp</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
function HomeComp() {
|
||||
let {compUuid} = useParams();
|
||||
const [perm, setPerm] = useState("")
|
||||
|
||||
const messageHandler = msg => {
|
||||
console.log("Received WS message:", msg);
|
||||
}
|
||||
|
||||
return <WSProvider url={`${vite_url.replace('http', 'ws')}/api/ws/competition/${compUuid}`} onmessage={messageHandler}>
|
||||
<WSStatus setPerm={setPerm}/>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home2 perm={perm}/>}/>
|
||||
<Route path="/test" element={<Test2/>}/>
|
||||
</Routes>
|
||||
</WSProvider>
|
||||
}
|
||||
|
||||
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 <div className="row">
|
||||
<h2 className="col">{name}</h2>
|
||||
<div className="col-auto" style={{margin: "auto 0"}}>Serveur: <ColoredCircle color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function Home2({perm}) {
|
||||
const nav = useNavigate();
|
||||
const {sendRequest} = useWS();
|
||||
|
||||
return <div className="row">
|
||||
<h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4>
|
||||
<div className="col">
|
||||
{perm === "admin" && <>
|
||||
<button className="btn btn-primary" onClick={() => nav("admin")}>Administration</button>
|
||||
<button className="btn btn-primary ms-3" onClick={() => nav("table")}>Table de marque</button>
|
||||
</>}
|
||||
{perm === "table" && <>
|
||||
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button>
|
||||
</>}
|
||||
</div>
|
||||
<button onClick={() => {
|
||||
sendRequest("getAllMatch", 1156)
|
||||
.then(data => {
|
||||
console.log("Received response for getAllMatch:", data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error in getAllMatch request:", err);
|
||||
})
|
||||
}}>Send Test
|
||||
</button>
|
||||
<button onClick={() => nav(-1)}>Go Back</button>
|
||||
<button onClick={() => nav("test")}>Go Test</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
function Test2() {
|
||||
let {compUuid} = useParams();
|
||||
const nav = useNavigate();
|
||||
return <div>
|
||||
<h2>Product ID: {compUuid}</h2>
|
||||
<button onClick={() => nav(-1)}>Go Back</button>
|
||||
</div>
|
||||
}
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user