diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 771cfc5..9bac398 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,7 @@ jobs: # transparency data even for private images, pass --force to cosign below. # https://github.com/sigstore/cosign - name: Sign the images with GitHub OIDC Token + if: github.event_name != 'pull_request' env: DIGEST: ${{ steps.build-and-push.outputs.digest }} TAGS: ${{ steps.meta.outputs.tags }} @@ -90,5 +91,5 @@ jobs: cosign sign --yes ${images} - name: Invoke deployment hook uses: distributhor/workflow-webhook@v3 - env: + with: webhook_url: ${{ secrets.WEBHOOK }} diff --git a/backend/src/util/SocketIOController.ts b/backend/src/util/SocketIOController.ts index f1f60e5..09e1edb 100644 --- a/backend/src/util/SocketIOController.ts +++ b/backend/src/util/SocketIOController.ts @@ -1,13 +1,13 @@ /****************************************************************************** * SocketIOController.ts * * * - * Copyright (c) 2022-2023 Robin Ferch * + * Copyright (c) 2022-2025 Robin Ferch * * https://robinferch.me * * This project is released under the MIT license. * ******************************************************************************/ import Core from "../Core.js"; import * as Http from "http"; -import {Server, Socket} from "socket.io"; +import {ExtendedError, Server, Socket} from "socket.io"; interface AuthenticatedSocket extends Socket { @@ -24,11 +24,11 @@ interface Player { class SocketIOController { - private httpServer: Http.Server; + private readonly httpServer: Http.Server; private io: Server; - private core: Core; + private readonly core: Core; private players: Player[] = []; @@ -68,14 +68,14 @@ class SocketIOController { data = data.replace("[", "") data = data.replace("]", "") let split = data.split(", ") - split.forEach((u) => { - let decodedMsg = u.split(";") + split.forEach((userSegment: string) => { + let decodedMsg = userSegment.split(";") const data = { "uuid": decodedMsg[0], "lat": parseFloat(decodedMsg[1]), "lon": parseFloat(decodedMsg[2]), "username": decodedMsg[3], - "server": decodedMsg[4] + "server": socket.id } usersFromServer.push(data) }) @@ -124,23 +124,19 @@ class SocketIOController { } }); - socket.on('serverStartup', (data) => { - if (socket.authenticated) { - if (this.players.findIndex((pl) => pl.server === data) != -1) { - let idx = this.players.findIndex((pl) => pl.server === data); - this.players.splice(idx, 1); - this.core.getLogger().debug("Removed player " + data) - } - } - }); - - socket.emit("playerLocations", JSON.stringify({ "type": "FeatureCollection", "features": [] })) }); + this.io.on("connection", (socket) => { + socket.on("disconnect", _ => { + const initialPlayerCount = this.players.length; + this.players = this.players.filter((p) => p.server !== socket.id); + this.core.getLogger().debug(`Socket ${socket.id} disconnected. Removed ${initialPlayerCount - this.players.length} player(s) associated with this socket.`); + }); + }); } public sendTeleportRequest(coords: Array, uuid: string) { @@ -149,8 +145,8 @@ class SocketIOController { } } -const authMiddleware = (socket: AuthenticatedSocket, next) => { - if (socket.handshake.auth && socket.handshake.auth.token) { +const authMiddleware = (socket: AuthenticatedSocket, next: (err?: ExtendedError) => void) => { + if (socket.handshake.auth?.token) { if (socket.handshake.auth.token === process.env.MINECRAFT_PLUGIN_TOKEN) { socket.authenticated = true; next(); diff --git a/backend/src/web/Web.ts b/backend/src/web/Web.ts index fe58548..326cd11 100644 --- a/backend/src/web/Web.ts +++ b/backend/src/web/Web.ts @@ -16,6 +16,7 @@ import { fileURLToPath } from "url"; import Core from '../Core.js'; import SocketIOController from "../util/SocketIOController.js"; import Routes from './routes/index.js'; +import type { RequestHandler } from 'express'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -40,12 +41,14 @@ class Web { public startWebserver() { this.app.use(express.json()); - this.app.use(session({ - secret: process.env.SESSION_SECRET, - resave: false, - saveUninitialized: true, - store: this.core.memoryStore - })); + this.app.use( + session({ + secret: process.env.SESSION_SECRET!, + resave: false, + saveUninitialized: true, + store: this.core.memoryStore, + }) as unknown as RequestHandler + ); this.app.use(cors()) this.app.use( @@ -58,9 +61,11 @@ class Web { })); this.core.getLogger().debug("Enabled keycloak-connect adapter") - this.app.use(fileUpload({ - limits: { fileSize: 10 * 1024 * 1024 }, - })); + this.app.use( + fileUpload({ + limits: { fileSize: 10 * 1024 * 1024 }, + }) as unknown as RequestHandler + ); this.server.listen(this.getPort(), () => { diff --git a/frontend/src/components/RegionView.jsx b/frontend/src/components/RegionView.jsx index 9b5be61..7be911d 100644 --- a/frontend/src/components/RegionView.jsx +++ b/frontend/src/components/RegionView.jsx @@ -1,7 +1,7 @@ /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + RegionView.jsx + + + - + Copyright (c) 2022-2024 Robin Ferch + + + Copyright (c) 2022-2025 Robin Ferch + + https://robinferch.me + + This project is released under the MIT license. + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ @@ -34,7 +34,6 @@ import {AiFillDelete, AiOutlineDelete, AiOutlineLink} from "react-icons/ai"; import {MdAdd, MdOutlineShareLocation, MdConstruction, MdDescription} from "react-icons/md"; import {useModals} from "@mantine/modals"; import {showNotification} from "@mantine/notifications"; -import {useKeycloak} from "@react-keycloak-fork/web"; import {FiLock} from "react-icons/fi"; import {useUser} from "../hooks/useUser"; import {IoMdFlag} from "react-icons/io"; @@ -47,6 +46,7 @@ import {GiPartyPopper} from "react-icons/gi"; import {TbFence} from "react-icons/tb"; import RegionImageView from "./RegionImageView"; import {useOidc} from "../oidc"; +import * as Sentry from "@sentry/react"; const RegionView = ({data, open, setOpen, setUpdateMap}) => { @@ -61,9 +61,9 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { const [plotType, setPlotType] = useState("normal"); const [isFinished, setisFinished] = useState(true); const [description, setDescription] = useState(""); - const [additionalBuildersArray, setAdditionalBuilders] = useState([]); + const [additionalBuilders, setAdditionalBuilders] = useState([]); - const { isUserLoggedIn, login, logout, oidcTokens, initializationError } = useOidc(); + const { isUserLoggedIn, login, oidcTokens} = useOidc(); const isAdmin = oidcTokens?.decodedIdToken.realm_access.roles.includes("mapadmin"); const user = useUser(); @@ -82,8 +82,6 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { if (!data?.id) return; const region_ = await axios.get(`/api/v1/region/${data.id}`); - setAdditionalBuilders(region_.data.additionalBuilder); - if (region_.data.isEventRegion) { setPlotType('event'); } else if (region_.data.isPlotRegion) { @@ -93,10 +91,33 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { } console.log(region_.data); // from the data get the userUUID and get the username from the playerdb api - const {data: mcApiData} = await axios.get(`https://playerdb.co/api/player/minecraft/${region_.data.userUUID}`); - region_.data.username = mcApiData.data.player.username; + try { + const {data: mcApiData} = await axios.get(`https://playerdb.co/api/player/minecraft/${region_.data.userUUID}`); + region_.data.username = mcApiData.data.player.username; + } catch (err) { + Sentry.captureException(err, { + tags: {section: 'owner‑lookup'}, + extra: {uuid: region_.data.userUUID, regionId: region_.data.id} + }); + } + + const tasks = region_.data.additionalBuilder.map(async addBuilder => { + try { + const { fetchedName } = (await axios.get(`https://playerdb.co/api/player/minecraft/${addBuilder.minecraftUUID}`)).data.player.username; + if (fetchedName !== addBuilder.username) addBuilder.username = fetchedName; + } catch (err) { + Sentry.captureException(err, { + tags: { section: 'builder‑lookup' }, + extra: { uuid: addBuilder.minecraftUUID, regionId: region_.data.id} + }); + } + }); + + await Promise.all(tasks); + console.log(region_.data); setRegion(region_.data); + setAdditionalBuilders(region_.data.additionalBuilder); // use the description only if it contains any words and not only html tags if (region_.data.description && region_.data.description.replace(/<[^>]*>/g, '').trim().length > 0) { @@ -198,7 +219,7 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { const addNewBuilder = async (newBuilder) => { const {data: mcApiData} = await axios.get(`https://playerdb.co/api/player/minecraft/${newBuilder.username}`); newBuilder.minecraftUUID = mcApiData.data.player.id; - setAdditionalBuilders([...additionalBuildersArray, newBuilder]); + setAdditionalBuilders([...additionalBuilders, newBuilder]); }; const addBuilderToDB = (builder) => { @@ -231,7 +252,7 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { }; const removeBuilder = (builder) => { - setAdditionalBuilders(additionalBuildersArray.filter(b => b.id !== builder.id)); + setAdditionalBuilders(additionalBuilders.filter(b => b.id !== builder.id)); }; const removeBuilderFromDB = (builder) => { @@ -285,8 +306,8 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { const onSave = async () => { const city = document.getElementById('city')?.value ?? region.city; const owner = document.getElementById('owner')?.value ?? region.username; - const addedBuilders = additionalBuildersArray.filter((item) => !region.additionalBuilder.includes(item)); - const removedBuilders = region.additionalBuilder.filter((item) => !additionalBuildersArray.includes(item)); + const addedBuilders = additionalBuilders.filter((item) => !region.additionalBuilder.includes(item)); + const removedBuilders = region.additionalBuilder.filter((item) => !additionalBuilders.includes(item)); setEditing(false); setLoading(true); for (let builder of addedBuilders) { @@ -423,7 +444,7 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { additionalElement={ } key={"additional-builders"} />