diff --git a/backend/src/main/kotlin/org/tormap/adapter/controller/RelayDetailsController.kt b/backend/src/main/kotlin/org/tormap/adapter/controller/RelayDetailsController.kt index 37d388f6..d714a7e0 100644 --- a/backend/src/main/kotlin/org/tormap/adapter/controller/RelayDetailsController.kt +++ b/backend/src/main/kotlin/org/tormap/adapter/controller/RelayDetailsController.kt @@ -30,4 +30,8 @@ class RelayDetailsController( @PostMapping("family/identifiers") fun getFamilyIdentifiers(@RequestBody familyIds: List) = relayDetailsRepositoryImpl.findFamilyIdentifiers(familyIds) + + @Operation(summary = "Return the nicknames of the relays that are associated with a list of relay details IDs.") + @PostMapping("relay/nicknames") + fun getRelayNicknames(@RequestBody ids: List) = relayDetailsRepositoryImpl.findAllByIdIn(ids).map { it.nickname } } diff --git a/backend/src/main/kotlin/org/tormap/database/repository/RelayDetailsRepository.kt b/backend/src/main/kotlin/org/tormap/database/repository/RelayDetailsRepository.kt index d3293338..d1d7084b 100644 --- a/backend/src/main/kotlin/org/tormap/database/repository/RelayDetailsRepository.kt +++ b/backend/src/main/kotlin/org/tormap/database/repository/RelayDetailsRepository.kt @@ -8,4 +8,5 @@ interface RelayDetailsRepository : JpaRepository { fun findAllByMonthEqualsAndFamilyEntriesNotNull(month: String): List fun findAllByMonthEqualsAndAutonomousSystemNumberNull(month: String): List fun findAllByFamilyId(familyId: Long): List + fun findAllByIdIn(ids: List): List } diff --git a/frontend/.env b/frontend/.env index b7ce867d..96c430a7 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,2 +1,4 @@ # The URL pointing to the backend instance (e.g. http://localhost:8080/) -VITE_BACKEND_API_URL=https://api.tormap.org \ No newline at end of file +VITE_BACKEND_API_URL=https://api.tormap.org +VITE_NOWPAYMENTS_API_KEY=BZ1BNEC-VDS47WP-PQB7G6F-9WTFHP8 +VITE_DONATION_BITCOIN_ADDRESS=bc1qkpwf376p75uyhqna87p22lt2t9zm99qflz39y3 diff --git a/frontend/package.json b/frontend/package.json index 77557bd3..6366eb2e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "name": "tormap", "version": "2.2.0", "private": true, + "type": "module", "scripts": { "start": "vite --port 3000", "build": "tsc && vite build", diff --git a/frontend/src/components/card/BitcoinCard.tsx b/frontend/src/components/card/BitcoinCard.tsx new file mode 100644 index 00000000..f569182d --- /dev/null +++ b/frontend/src/components/card/BitcoinCard.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import LightningIcon from '@mui/icons-material/FlashOn'; +import WalletIcon from '@mui/icons-material/Wallet'; +import {Stack} from '@mui/material'; + +export default function BitcoinCard() { + const bitcoinAddress = import.meta.env.VITE_DONATION_BITCOIN_ADDRESS + + const handleCopyAddress = () => { + navigator.clipboard.writeText(bitcoinAddress); + }; + + const openInWallet = () => { + window.open(`bitcoin:${bitcoinAddress}`, '_blank'); + }; + + return ( + + + + Bitcoin + + + + {bitcoinAddress} + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/components/dialogs/AboutInformation.tsx b/frontend/src/components/dialogs/AboutInformation.tsx index 85361783..c1dc84cc 100644 --- a/frontend/src/components/dialogs/AboutInformation.tsx +++ b/frontend/src/components/dialogs/AboutInformation.tsx @@ -22,8 +22,11 @@ import { import React, {useState} from "react"; import {ContributorCard} from "../card/ContributorCard"; -import {BitcoinIcon} from "../icons/BitcoinIcon"; import {ExternalLink} from "../link/ExternalLink"; +import BitcoinCard from "../card/BitcoinCard"; +import CardContent from "@mui/material/CardContent"; +import LaunchIcon from "@mui/icons-material/Launch"; +import Card from "@mui/material/Card"; /** * A component for displaying information about TorMap @@ -35,6 +38,8 @@ export const AboutInformation: React.FunctionComponent = () => { // App context const theme = useTheme() const isLargeScreen = useMediaQuery(theme.breakpoints.up("lg")) + const nowpaymentsApiKey = import.meta.env.VITE_NOWPAYMENTS_API_KEY + const nowpaymentsDonationUrl = `https://nowpayments.io/donation?api_key=${nowpaymentsApiKey}&source=lk_donation&medium=referral` return ( <> @@ -70,10 +75,6 @@ export const AboutInformation: React.FunctionComponent = () => { }/> - - }/> - { TorMap is a world map displaying approximate locations where Tor relays are being hosted. You can group, filter and analyze thousands of Tor relays, which route anonymous internet traffic daily. The historic state of the network can be viewed for any day between October 2007 - and today.
We developed TorMap for our practical P4-Project at the Initially TorMap was developed by us at the as part of - the . It was + the . The project was supervised by from { roles={["Server Admin"]} /> +

Donations

+ + If you like our project and want to support us, you can donate to us via Bitcoin or other + cryptocurrencies. Your donation helps us to cover server costs and keep the project running. + + + + + + + Other cryptocurrencies + + + + + + + We use IP geolocation data by , + relayCoordinatesMap: Map, singleColor: boolean, onMarkerClick: (e: LeafletMouseEvent) => void, ): LayerGroup => { - const defaultLayer = new LayerGroup() - const exitLayer = new LayerGroup() - const guardLayer = new LayerGroup() - const defaultMarkerLayer = new LayerGroup([defaultLayer, guardLayer, exitLayer]) - latLonMap.forEach((coordinate, key) => { - coordinate.forEach(relay => { - let color = Colors.Default - let layer = defaultLayer - switch (getRelayType(relay)) { - case RelayType.Exit: { - color = Colors.Exit - layer = exitLayer - break - } - case RelayType.Guard: { - color = Colors.Guard - layer = guardLayer - break - } - } - if (singleColor) color = "#989898" + const layer = new LayerGroup() + relayCoordinatesMap.forEach((relaysAtCoordinates, coordinatesKey) => { + if (relaysAtCoordinates.length == 0) return - circleMarker( - [relay.lat, relay.long], - { - radius: 1, - className: key, - color: color, - weight: 3, - }, - ) - .on("click", onMarkerClick) - .addTo(layer) - }) + const { + mostImportantRelay, + marker + } = addRelayMarker(relaysAtCoordinates, layer, singleColor, coordinatesKey, onMarkerClick) + + addRelayNicknameTooltip(relaysAtCoordinates, mostImportantRelay, marker, layer); }) - return defaultMarkerLayer + return layer +} + +function addRelayMarker(relaysAtCoordinates: RelayLocationDto[], targetLayer: LayerGroup, singleColor: boolean, coordinatesKey: string, onMarkerClick: (e: LeafletMouseEvent) => void) { + let mostImportantRelay = relaysAtCoordinates[0] + for (const relay of relaysAtCoordinates) { + if (getRelayType(relay) === RelayType.Exit) { + mostImportantRelay = relay + break + } else if (getRelayType(relay) === RelayType.Guard) { + mostImportantRelay = relay + } + } + let color = Colors.Default + switch (getRelayType(mostImportantRelay)) { + case RelayType.Exit: { + color = Colors.Exit + break + } + case RelayType.Guard: { + color = Colors.Guard + break + } + } + if (singleColor) color = "#989898" + + const marker = circleMarker( + [mostImportantRelay.lat, mostImportantRelay.long], + { + radius: 1, + className: coordinatesKey, + color: color, + weight: 3, + }, + ) + .on("click", onMarkerClick) + .addTo(targetLayer) + return {mostImportantRelay, marker}; +} + +function addRelayNicknameTooltip(relays: RelayLocationDto[], mostImportantRelay: RelayLocationDto, marker: CircleMarker, targetLayer: LayerGroup) { + const knownRelayDetailIds: number[] = relays.map(relay => relay.detailsId) + .filter(id => id !== undefined && id !== null) as number[] + + // Create an invisible larger circle around the marker + const hoverRadius = 15; + const hoverMarker = circleMarker( + [mostImportantRelay.lat, mostImportantRelay.long], + { + radius: hoverRadius, + fillOpacity: 0, + fill: false, + stroke: false, + }, + ) + .on('mouseover', async function (this: L.Marker, e) { + let tooltip = `${relays.length} relays` + const maxNicknamesShownFully = 3 + if (knownRelayDetailIds.length <= maxNicknamesShownFully) { + await backend.post('/relay/details/relay/nicknames', knownRelayDetailIds).then(response => { + tooltip = response.data.join(", ") + }) + } + marker.bindTooltip(tooltip, {permanent: true, direction: 'top'}).openTooltip(); + }) + .on('mouseout', function (this: L.Marker, e) { + marker.unbindTooltip(); + }) + .on("click", function (this: L.Marker, e) { + marker.fire('click'); // Trigger the click event of the actual marker + }) + .addTo(targetLayer) } /**