diff --git a/.gitignore b/.gitignore index 85e7c1dfcb7fbb33f932c81024018cd8c10519da..298ba272c72c5beb4c793063578ba361d0836a3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /.idea/ +/banguardian-ui/dist +/banguardian-ui/node_modules \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 30cf57ed7cba5157ad5f7c05ede08adde945ada6..0000000000000000000000000000000000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/ban-guardian.iml b/.idea/ban-guardian.iml deleted file mode 100644 index d6ebd4805981b8400db3e3291c74a743fef9a824..0000000000000000000000000000000000000000 --- a/.idea/ban-guardian.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 7ee9818655d082c0b6c3496f8e07ffc9bb4a45c9..0000000000000000000000000000000000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/dataSources/data_sources_history.xml b/.idea/dataSources/data_sources_history.xml deleted file mode 100644 index 09de4bb7528c5065094b30cb3e6835d1b44d0f1a..0000000000000000000000000000000000000000 --- a/.idea/dataSources/data_sources_history.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - mariadb - true - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/banguard - banguardianu1 - - $ProjectFileDir$ - - - - - - mariadb - true - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/banguard - c10_banguard - - $ProjectFileDir$ - - - - - - mariadb - true - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/c10_banguard - c10_banguard - - $ProjectFileDir$ - - - - - - mariadb - true - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/c10_banguard - c10_banguard - - $ProjectFileDir$ - - - - - - mariadb - true - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/c10_banguard - c10_banguard - - $ProjectFileDir$ - - - - - - mariadb - true - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/c10_banguard - c10_banguard - - $ProjectFileDir$ - - - - - - #@ - ` - - - mariadb - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/c10_banguard - - - - - - master_key - c10_banguard - - - - - - $ProjectFileDir$ - - - - - - mariadb - true - true - org.mariadb.jdbc.Driver - jdbc:mariadb://192.168.1.206:3306/c10_banguard - c10_banguard - - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index d5f2fbd749450dab249e49f87774ca0581f5339c..0000000000000000000000000000000000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 712ab9d985c20018a0c97b93d2148ac1ffe588a5..0000000000000000000000000000000000000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 3d40a0c64cd81db420f0ad3fa19cdfc4f0d78785..0000000000000000000000000000000000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 9b141af657f338f4173325287c2cbc512ee358e9..0000000000000000000000000000000000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f4cb416c083d265558da75d457237d671..0000000000000000000000000000000000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 63417b9f5af420971f593d167bf4fe7bc955020c..0000000000000000000000000000000000000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - { - "lastFilter": { - "state": "OPENED", - "assignee": { - "type": "org.jetbrains.plugins.gitlab.mergerequest.ui.filters.GitLabMergeRequestsFiltersValue.MergeRequestsMemberFilterValue.MergeRequestsAssigneeFilterValue", - "username": "pratyush", - "fullname": "pratyush" - } - } -} - { - "selectedUrlAndAccountId": { - "first": "https://gitlab.erpratyush.me/pratyush/ban-guardian.git", - "second": "db76e022-5ac0-4e45-8fdd-277752c65b15" - } -} - {} - { - "isMigrated": true -} - - - - - - - - { - "associatedIndex": 0, - "fromUser": false -} - - - - - - - - - - - - - - - - - - - - - - - - 1775162724013 - - - - - - - - - - - - - - - - - handleDateChange('start', e.target.value)} + /> + +
+ + handleDateChange('end', e.target.value)} + /> +
+ +
+ +
+ + + + {/* Desktop Table View */} +
+ + + + + + + + + + + {events.length === 0 ? ( + + + + ) : ( + events.map((event) => ( + + + + + + + )) + )} + +
TimestampIP AddressJailReporting Server
No events found
+ {new Date(event.timestamp).toLocaleString()} + + + {event.jail}{event.serverHostname}
+
+ + {/* Mobile/Tablet Card View */} +
+ {events.length === 0 ? ( +
+ No events found +
+ ) : ( + events.map((event) => ( + + )) + )} +
+ + {/* Pagination Controls */} + {pageData && ( + + )} + + ); +}; + +export default BanHistory; diff --git a/banguardian-ui/src/components/IpDetailView.tsx b/banguardian-ui/src/components/IpDetailView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..020203e7f8b42a4fda55da78353f2746d8d1551d --- /dev/null +++ b/banguardian-ui/src/components/IpDetailView.tsx @@ -0,0 +1,194 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import {type BanEvent, banEventService, type Page, type LiveBan} from '../services/banEventService'; +import { ShieldOff, ShieldAlert } from 'lucide-react'; +import Pagination from './common/Pagination'; +import BanCard from './common/BanCard'; +import CountryFlag from './common/CountryFlag'; + +const IpDetailView: React.FC = () => { + const { ip } = useParams<{ ip: string }>(); + const navigate = useNavigate(); + const [pageData, setPageData] = useState | null>(null); + const [liveStatus, setLiveStatus] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [actionLoading, setActionLoading] = useState(null); + const [page, setPage] = useState(0); + + const fetchEvents = useCallback(async () => { + if (!ip) return; + try { + setLoading(true); + const [historyData, statusData] = await Promise.all([ + banEventService.getIpHistory(ip, { + page, + size: 10 + }), + banEventService.getIpLiveStatus(ip) + ]); + setPageData(historyData); + setLiveStatus(statusData); + } catch (err) { + setError(`Failed to load data for ${ip}`); + console.error(err); + } finally { + setLoading(false); + } + }, [ip, page]); + + useEffect(() => { + fetchEvents(); + }, [fetchEvents]); + + const handleBanManual = async () => { + if (!ip) return; + const jail = prompt('Enter jail name:', 'sshd'); + if (!jail) return; + + try { + setActionLoading('manual-ban'); + await banEventService.reportEvent(ip, jail, 'MANUAL', 'ban'); + await fetchEvents(); + } catch (err) { + alert('Failed to ban IP'); + console.error(err); + } finally { + setActionLoading(null); + } + }; + + const handleUnbanLive = async (liveBan: LiveBan) => { + if (!confirm(`Are you sure you want to unban ${liveBan.ip} from ${liveBan.serverHostname} (${liveBan.jail})?`)) return; + + try { + setActionLoading(liveBan.id); + await banEventService.reportEvent(liveBan.ip, liveBan.jail, liveBan.serverHostname, 'unban'); + await fetchEvents(); + } catch (err) { + alert('Failed to unban IP'); + console.error(err); + } finally { + setActionLoading(null); + } + }; + + if (loading && !pageData) return
Loading...
; + if (error) return
{error}
; + + const events = pageData?.content || []; + + return ( +
+
+
+ +

+ History for {ip} + {events.length > 0 && } +

+
+ +
+ {liveStatus.length > 0 ? ( + liveStatus.map(ban => ( + + )) + ) : ( + + )} +
+
+ + {/* Desktop Table View */} +
+ + + + + + + + + + {events.length === 0 ? ( + + + + ) : ( + events.map((event) => ( + + + + + + )) + )} + +
TimestampJailReporting Server
No events found
+ {new Date(event.timestamp).toLocaleString()} + {event.jail}{event.serverHostname}
+
+ + {/* Mobile/Tablet Card View */} +
+ {events.length === 0 ? ( +
+ No events found +
+ ) : ( + events.map((event) => ( + {}} // IP is already known in this view + /> + )) + )} +
+ + {/* Pagination Controls */} + {pageData && ( + + )} +
+ ); +}; + +export default IpDetailView; diff --git a/banguardian-ui/src/components/LiveView.tsx b/banguardian-ui/src/components/LiveView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8f46f5a2f12405cc0af9e086b7027b4e5076a331 --- /dev/null +++ b/banguardian-ui/src/components/LiveView.tsx @@ -0,0 +1,170 @@ +import React, { useEffect, useState } from 'react'; +import { type LiveBan, banEventService } from '../services/banEventService'; +import SockJS from 'sockjs-client'; +import { Client } from '@stomp/stompjs'; +import { ShieldOff } from 'lucide-react'; +import IPLink from './common/IPLink'; +import BanCard from './common/BanCard'; + +interface LiveViewProps { + onIpClick: (ip: string) => void; +} + +const LiveView: React.FC = ({ onIpClick }) => { + const [bans, setBans] = useState([]); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + // Initial fetch + const fetchInitialBans = async () => { + try { + const currentlyBanned = await banEventService.getLiveEvents(); + setBans(currentlyBanned); + } catch (err) { + setError('Failed to load live data'); + console.error(err); + } finally { + setLoading(false); + } + }; + + fetchInitialBans(); + + // Setup WebSocket + const socket = new SockJS('/ws'); + const stompClient = new Client({ + webSocketFactory: () => socket, + onConnect: () => { + stompClient.subscribe('/topic/live-bans', (message) => { + const updatedBans = JSON.parse(message.body); + setBans(updatedBans); + }); + }, + onStompError: (frame) => { + console.error('Broker reported error: ' + frame.headers['message']); + console.error('Additional details: ' + frame.body); + }, + }); + + stompClient.activate(); + + return () => { + stompClient.deactivate(); + }; + }, []); + + const handleUnban = async (ban: LiveBan) => { + if (!confirm(`Are you sure you want to unban ${ban.ip}?`)) return; + + try { + setActionLoading(ban.id); + // We need to report an UNBAN event to the server + await banEventService.reportEvent(ban.ip, ban.jail, ban.serverHostname, 'unban'); + // No need to fetchBans() here as the WebSocket will push the update + } catch (err) { + alert('Failed to unban IP'); + console.error(err); + } finally { + setActionLoading(null); + } + }; + + if (loading && bans.length === 0) return
Loading live status...
; + if (error) return
{error}
; + + return ( +
+

Currently Banned IPs

+ + {/* Desktop Table View */} +
+ + + + + + + + + + + + {bans.length === 0 ? ( + + + + ) : ( + bans.map((ban) => ( + + + + + + + + )) + )} + +
IP AddressBanned SinceJailServerActions
No IPs currently banned
+ + + {new Date(ban.bannedAt).toLocaleString()} + {ban.jail}{ban.serverHostname} + +
+
+ + {/* Mobile Card View */} +
+ {bans.length === 0 ? ( +
+ No IPs currently banned +
+ ) : ( + bans.map((ban) => ( + handleUnban(ban)} + disabled={actionLoading === ban.id} + title="Unban IP" + className="bg-green-600 hover:bg-green-700 text-white p-1.5 rounded-lg shadow transition-colors disabled:opacity-50" + > + {actionLoading === ban.id ? ( + ... + ) : ( + + )} + + } + /> + )) + )} +
+
+ ); +}; + +export default LiveView; diff --git a/banguardian-ui/src/components/MockEvents.tsx b/banguardian-ui/src/components/MockEvents.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5cfa6ac289113169c1a19adea7f7f982f52ca3a5 --- /dev/null +++ b/banguardian-ui/src/components/MockEvents.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; +import { banEventService } from '../services/banEventService'; + +const MockEvents: React.FC = () => { + const [ip, setIp] = useState('1.2.3.4'); + const [jail, setJail] = useState('sshd'); + const [server, setServer] = useState('server1'); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState<{ text: string, type: 'success' | 'error' } | null>(null); + + const handleReport = async (type: 'ban' | 'unban') => { + setLoading(true); + setMessage(null); + try { + await banEventService.reportEvent(ip, jail, server, type); + setMessage({ text: `Successfully reported ${type} from ${server} for ${ip}`, type: 'success' }); + } catch (err) { + setMessage({ text: `Failed to report ${type}: ${err}`, type: 'error' }); + } finally { + setLoading(false); + } + }; + + return ( +
+

Mock Ban/Unban Events

+
+
+ + setIp(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g. 1.2.3.4" + /> +
+
+ + setJail(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g. sshd" + /> +
+
+ + setServer(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g. server1 (Use 'MANUAL' for manual source)" + /> +
+
+ Note: Use 'MANUAL' as the server hostname to simulate a manual user action. +
+ + {message && ( +
+ {message.text} +
+ )} + +
+ + +
+
+
+ ); +}; + +export default MockEvents; diff --git a/banguardian-ui/src/components/common/BanCard.tsx b/banguardian-ui/src/components/common/BanCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ce3cd5b040987820b8418dfc58d809fa13c397a9 --- /dev/null +++ b/banguardian-ui/src/components/common/BanCard.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import IPLink from './IPLink'; + +interface BanCardProps { + ip: string; + timestamp: string; + isBan: boolean; + jail: string; + server: string; + countryCode?: string; + countryName?: string; + onIpClick: (ip: string) => void; + actions?: React.ReactNode; +} + +const BanCard: React.FC = ({ + ip, + timestamp, + isBan, + jail, + server, + countryCode, + countryName, + onIpClick, + actions +}) => { + return ( +
+
+ {/* IP and Gradient Indicator */} +
+ +
+
+ + {/* Time */} +
+ {new Date(timestamp).toLocaleString([], { dateStyle: 'short', timeStyle: 'short' })} +
+ + {/* Jail & Server */} +
+
+ Jail: + {jail} +
+
+ Server: + {server} +
+
+ + {/* Actions */} + {actions && ( +
+ {actions} +
+ )} +
+
+ ); +}; + +export default BanCard; diff --git a/banguardian-ui/src/components/common/CountryFlag.tsx b/banguardian-ui/src/components/common/CountryFlag.tsx new file mode 100644 index 0000000000000000000000000000000000000000..adaec0a060ca390c0b012f591bf475acf44e0d4f --- /dev/null +++ b/banguardian-ui/src/components/common/CountryFlag.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface CountryFlagProps { + countryCode?: string; + countryName?: string; + className?: string; + showName?: boolean; +} + +const CountryFlag: React.FC = ({ countryCode, countryName, className = "", showName = false }) => { + if (!countryCode) return null; + + // Use flagcdn.com for flags (SVG) + // const flagUrl = `https://flagcdn.com/${countryCode.toLowerCase()}.svg`; + const flagUrl = `/images/flags/${countryCode.toLowerCase()}.svg`; + + return ( +
+ {countryName { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {showName && countryName && ( + + {countryName} + + )} +
+ ); +}; + +export default CountryFlag; diff --git a/banguardian-ui/src/components/common/IPLink.tsx b/banguardian-ui/src/components/common/IPLink.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4899f0db7d67952c91237d7c262166d30c441c13 --- /dev/null +++ b/banguardian-ui/src/components/common/IPLink.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import CountryFlag from './CountryFlag'; + +interface IPLinkProps { + ip: string; + onClick: (ip: string) => void; + countryCode?: string; + countryName?: string; + className?: string; +} + +const IPLink: React.FC = ({ ip, onClick, countryCode, countryName, className = "" }) => { + return ( +
+ + +
+ ); +}; + +export default IPLink; diff --git a/banguardian-ui/src/components/common/Pagination.tsx b/banguardian-ui/src/components/common/Pagination.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0828b01038a0766ae0e757139a61ad181d5b4979 --- /dev/null +++ b/banguardian-ui/src/components/common/Pagination.tsx @@ -0,0 +1,90 @@ +import React from 'react'; + +interface PaginationProps { + currentPage: number; + totalPages: number; + totalElements: number; + size: number; + onPageChange: (page: number) => void; +} + +const Pagination: React.FC = ({ + currentPage, + totalPages, + totalElements, + size, + onPageChange +}) => { + if (totalPages <= 1) return null; + + return ( +
+
+ + +
+
+
+

+ Showing {currentPage * size + 1} to{' '} + + {Math.min((currentPage + 1) * size, totalElements)} + {' '} + of {totalElements} results +

+
+
+ +
+
+
+ ); +}; + +export default Pagination; diff --git a/banguardian-ui/src/index.css b/banguardian-ui/src/index.css index 5fb33130220482ba51683f2f43660cca10f174d9..a79373804dd19f948865a3260f907d1a0be0fe67 100644 --- a/banguardian-ui/src/index.css +++ b/banguardian-ui/src/index.css @@ -1,111 +1,17 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; +@import "tailwindcss"; - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; +/* Ensure high-contrast or custom colors if tailwind 4.x defaults differ */ +@theme { + --color-brand-blue: #2563eb; + --color-brand-dark: #111827; } body { + @apply bg-gray-100; margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} \ No newline at end of file diff --git a/banguardian-ui/src/main.tsx b/banguardian-ui/src/main.tsx index bef5202a32cbd0632c43de40f6e908532903fd42..ade9d64038118d61925891de7642f1b5cf1cbedf 100644 --- a/banguardian-ui/src/main.tsx +++ b/banguardian-ui/src/main.tsx @@ -1,10 +1,13 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/banguardian-ui/src/services/banEventService.ts b/banguardian-ui/src/services/banEventService.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d847945fc3032c7a6d6f90aa781a60efd509cf6 --- /dev/null +++ b/banguardian-ui/src/services/banEventService.ts @@ -0,0 +1,109 @@ +import EventType from "./eventType.ts"; + +export interface LiveBan { + id: number; + ip: string; + jail: string; + serverHostname: string; + countryCode?: string; + countryName?: string; + bannedAt: string; +} + +export interface BanEvent { + id: number; + ip: string; + jail: string; + serverHostname: string; + countryCode?: string; + countryName?: string; + eventType: keyof typeof EventType; + timestamp: string; +} + +export interface Page { + content: T[]; + totalPages: number; + totalElements: number; + size: number; + number: number; +} + +export interface BanEventFilters { + searchTerm?: string; + startDate?: string; + endDate?: string; + page?: number; + size?: number; +} + +const API_BASE_URL = '/api'; + +export const banEventService = { + getAllEvents: async (filters: BanEventFilters = {}): Promise> => { + const params = new URLSearchParams(); + if (filters.searchTerm) params.append('searchTerm', filters.searchTerm); + if (filters.startDate) params.append('startDate', filters.startDate); + if (filters.endDate) params.append('endDate', filters.endDate); + if (filters.page !== undefined) params.append('page', filters.page.toString()); + if (filters.size !== undefined) params.append('size', filters.size.toString()); + + const response = await fetch(`${API_BASE_URL}/bans?${params.toString()}`); + if (!response.ok) { + throw new Error('Failed to fetch ban events'); + } + return response.json(); + }, + + getLiveEvents: async (): Promise => { + const response = await fetch(`${API_BASE_URL}/bans/live`); + if (!response.ok) { + throw new Error('Failed to fetch live ban events'); + } + return response.json(); + }, + + getIpLiveStatus: async (ip: string): Promise => { + const response = await fetch(`${API_BASE_URL}/bans/live/${ip}`); + if (!response.ok) { + throw new Error(`Failed to fetch live status for ${ip}`); + } + return response.json(); + }, + + getIpHistory: async (ip: string, filters: BanEventFilters = {}): Promise> => { + const params = new URLSearchParams({ ip }); + if (filters.startDate) params.append('startDate', filters.startDate); + if (filters.endDate) params.append('endDate', filters.endDate); + if (filters.page !== undefined) params.append('page', filters.page.toString()); + if (filters.size !== undefined) params.append('size', filters.size.toString()); + + const response = await fetch(`${API_BASE_URL}/bans/history?${params.toString()}`); + if (!response.ok) { + throw new Error(`Failed to fetch history for ${ip}`); + } + return response.json(); + }, + + reportEvent: async (ip: string, jail: string, server: string, type: 'ban' | 'unban'): Promise => { + const response = await fetch(`${API_BASE_URL}/report`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ip, jail, server, type }), + }); + if (!response.ok) { + throw new Error(`Failed to report ${type} event`); + } + }, + + manualUnban: async (id: number): Promise => { + const response = await fetch(`${API_BASE_URL}/report/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to manually unban IP'); + } + } +}; diff --git a/banguardian-ui/src/services/eventType.ts b/banguardian-ui/src/services/eventType.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6fbcdc8c1268e6e13d31eb539bce5c7b8f8e64e --- /dev/null +++ b/banguardian-ui/src/services/eventType.ts @@ -0,0 +1,6 @@ +export const EventType = { + BAN: 'BAN', + UNBAN: 'UNBAN' +} as const; + +export default EventType; \ No newline at end of file diff --git a/banguardian-ui/vite.config.ts b/banguardian-ui/vite.config.ts index 8b0f57b91aeb45c54467e29f983a0893dc83c4d9..85085aa30779cdbd39e728a3a9078e99be009f4e 100644 --- a/banguardian-ui/vite.config.ts +++ b/banguardian-ui/vite.config.ts @@ -1,7 +1,23 @@ -import { defineConfig } from 'vite' +import {defineConfig} from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + // base: './', + plugins: [react(), tailwindcss()], + define: { + global: 'globalThis', + }, + server: { + proxy: { + "/api":{ + target: "http://localhost:8080", + }, + "/ws": { + target: "http://localhost:8080/", + ws: true + } + } + } }) diff --git a/k3s/deployment.yaml b/k3s/deployment.yaml index 689beda0c6bc6e817dfc1ed84e6dc09be643166a..ee5640260ef86fc225efa0f7632d394ff7968009 100644 --- a/k3s/deployment.yaml +++ b/k3s/deployment.yaml @@ -66,6 +66,20 @@ kind: Ingress metadata: name: banguardian-ingress annotations: + # 1. Enable Session Affinity (CRITICAL for SockJS with multiple replicas) + nginx.ingress.kubernetes.io/affinity: "cookie" + nginx.ingress.kubernetes.io/session-cookie-name: "route" + nginx.ingress.kubernetes.io/session-cookie-expires: "172800" + nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" + + # 2. Increase Timeouts (Prevents connection drops during idle periods) + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + + # 3. WebSocket specific headers (Handled automatically by modern NGINX ingress, but good to keep in mind) + # nginx.ingress.kubernetes.io/websocket-services: "banguardian-service" + + # Your existing annotation # Optional: Increases max upload size (useful for Spring Boot file uploads) nginx.ingress.kubernetes.io/proxy-body-size: "50m" spec: