import React from "react"; import { createRoot } from "react-dom/client"; import { useState, useEffect } from 'react'; import { ApiaryAPI, LoginAttempt, DummyApiaryAPIClient, ApiaryAPIClient, TotalStats, StatsResult, StatsType } from "./api"; import { BrowserRouter, NavLink, Routes, Route } from "react-router"; import { Chart as ChartJS, Tooltip, ArcElement, Legend, ChartOptions } from "chart.js"; import { Pie } from "react-chartjs-2"; import humanizeDuration from "humanize-duration"; import * as classes from "../css/style.module.css"; ChartJS.register(Tooltip, ArcElement, Legend); console.log(classes); interface AppProps { api: ApiaryAPI } let chartColors = [ "#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50", "#f1c40f", "#e67e22", "#e74c3c", "#ecf0f1", "#95a5a6", "#f39c12", "#d35400", "#c0392b", "#bdc3c7", "#7f8c8d", ] export function App({ api }: AppProps) { const [mode, setMode] = useState("light"); const headerProps: HeaderMenuProps = { title: "apiary.home.2rjus.net", items: [ { name: "Totals", path: "/", }, { name: "Passwords", path: "/stats/password", }, { name: "Usernames", path: "/stats/username", }, { name: "IPs", path: "/stats/ip", }, { name: "Live", path: "/live", }, { name: "Query", path: "/query", } ] } const onSelectMode = (mode: string) => { setMode(mode); if (mode === "dark") { document.body.classList.add(classes.dark_mode); } else { document.body.classList.remove(classes.dark_mode); } } useEffect(() => { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => onSelectMode(e.matches ? "dark" : "light")); onSelectMode(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') return () => { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', (e) => onSelectMode(e.matches ? "dark" : "light")); } }, []) return ( <>
} /> } /> } /> } /> } /> } />
); } export interface StatsProps { api: ApiaryAPI type: StatsType } export function Stats({ api, type }: StatsProps) { const [stats, setStats] = useState(null) const [currentType, setCurrentType] = useState(type) useEffect(() => { async function getStats() { try { let newStats = await api.stats(type, 10); if (JSON.stringify(newStats) !== JSON.stringify(stats)) { setStats(newStats) } } catch (e) { console.log("Error getting stats", e) } } if (currentType !== type) { setCurrentType(type) getStats() } if (stats === null) { getStats() } const interval = setInterval(() => { getStats() }, 60000) return () => { clearInterval(interval); } }, [stats, type]) return ( <> {(stats != null && stats.length > 0) ? :

Loading...

} ); } export interface StatsPieProps { data: StatsResult[] } export function StatsPie({ data }: StatsPieProps) { const labels = data.map((d) => d.name); const values = data.map((d) => d.count); const piedata = { labels, datasets: [{ label: "# of attempts", data: values, backgroundColor: chartColors, borderWidth: 1 }] }; console.log(piedata) const getTextColor = () => { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'white' : 'black' } const options: ChartOptions<"pie"> = { plugins: { legend: { display: true, align: "start", labels: { color: getTextColor(), } } } } return (
) } export function Home({ api }: AppProps) { const [totals, setTotals] = useState(null) useEffect(() => { async function getTotals() { let totals = await api.totals(); setTotals(totals); } if (!totals) { getTotals(); } const interval = setInterval(() => { getTotals(); }, 5000) return () => { clearInterval(interval); } }) return ( <> {totals ? :

Loading...

} ); } export interface TotalsProps { totals: TotalStats } export function Totals({ totals }: TotalsProps) { return (

Unique passwords

Unique username

Unique IPs

Total attempts

{totals.password}

{totals.username}

{totals.ip}

{totals.attempts}

) } export function Live({ api }: AppProps) { let list: LoginAttempt[] = []; let [liveList, setLiveList] = useState(list); useEffect(() => { const cleanup = api.live((a) => { setLiveList((list) => { return [a, ...list]; }); }); return cleanup }, [liveList, api]); return ( <> ); } export interface LiveListProps { list: LoginAttempt[] }; export interface DateTDProps { date: Date now: Date } export function DateTD({ date, now }: DateTDProps) { const [displayDate, setDisplayDate] = useState(date.toLocaleString()); useEffect(() => { if (now.getTime() - date.getTime() < 14400000) { const newDate = humanizeDuration(now.getTime() - date.getTime(), { largest: 1, round: true }) + " ago"; if (newDate !== displayDate) { setDisplayDate(newDate); } } }, [displayDate, now]) return ( {displayDate} ) } export function LiveList({ list }: LiveListProps) { const [now, setNow] = useState(new Date()) let items = list.map((a) => { const attemptDate = new Date(a.date); const key = `${a.username}-${a.password}-${a.remoteIP}-${a.date}`; return ( {a.username} {a.password} {a.remoteIP} {a.country} ) }) useEffect(() => { const interval = setInterval(() => { setNow(new Date()) }, 1000) return () => { clearInterval(interval) } }, [now]) return ( <> {items}
Date Username Password IP Country
) }; export function Query({ api }: AppProps) { const [liveList, setLiveList] = useState([]); const [queryErr, setQueryErr] = useState(null); async function handleSubmit(event: React.FormEvent) { event.preventDefault(); const value = event.currentTarget.query.value; if (value === "") { setQueryErr(new Error("Query cannot be empty")); return } try { const results = await api.query("", value) setQueryErr(null); setLiveList(results); } catch (e) { if (e instanceof Error) { setQueryErr(e); } } } return ( <>
{queryErr ? : null} ); } interface ErrorBoxProps { message: string | null }; export function ErrorBox({ message }: ErrorBoxProps) { return (

Error: {message}

) } export function Header() { return (
); } interface HeaderItem { name: string path: string } interface HeaderMenuProps { title: string items: Array } export function HeaderMenu({ title, items }: HeaderMenuProps) { const menuItems = items.map((item) => { return (
  • isActive ? classes.menu_link_active : classes.menu_link}>{item.name}
  • ) }) return (

    {title}

    ) } export interface SubMenuProps { items: Array<{ name: string, active: () => boolean, onClick: () => void }> } export function SubMenu({ items }: SubMenuProps) { return ( ) } const rootElement = document.getElementById('root'); if (rootElement) { const root = createRoot(rootElement); let api: ApiaryAPI; if (process.env.NODE_ENV === "production") { api = new ApiaryAPIClient(); } else { api = new DummyApiaryAPIClient(); } root.render( ); }