From 7e5c12c1d13a04240191f103a17bb39f8a857846 Mon Sep 17 00:00:00 2001 From: bagas Date: Fri, 2 Jan 2026 17:06:02 +0700 Subject: [PATCH] feat: add dashboard page --- app/dashboard/page.tsx | 557 ++++++++++++++++++++++ app/page.tsx | 1 + app/settings/page.tsx | 882 +++++++++++++++++++++++++++++++++++ components/tunnel-config.tsx | 20 +- 4 files changed, 1451 insertions(+), 9 deletions(-) create mode 100644 app/dashboard/page.tsx create mode 100644 app/settings/page.tsx diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..d9e12b4 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,557 @@ +"use client" + +import { useEffect, useState } from "react" +import Link from "next/link" +import TunnelConfig, { type TunnelConfig as TunnelConfigType, type Server } from "@/components/tunnel-config" +import UserMenu from "@/components/user-menu" +import { authClient } from "@/lib/auth-client" + +const defaultConfig: TunnelConfigType = { + type: "http", + serverPort: 443, + localPort: 8000, +} + +type ActiveConnection = { + id: string + name: string + serverLabel: string + protocol: TunnelConfigType["type"] + localPort: number + serverPort: number + remote: string + status: "connected" | "pending" | "error" + latencyMs: number | null + dataInOut: string + startedAgo: string +} + +export default function DashboardPage() { + const [selectedServer, setSelectedServer] = useState(null) + const [tunnelConfig, setTunnelConfig] = useState(defaultConfig) + const [statusMessage, setStatusMessage] = useState(null) + const [activeConnections, setActiveConnections] = useState([ + { + id: "conn-1", + name: "Frontend Preview", + serverLabel: "Singapore", + protocol: "http", + localPort: 3000, + serverPort: 443, + remote: "https://sgp.tunnl.live", + status: "connected", + latencyMs: 34, + dataInOut: "1.2 GB", + startedAgo: "3h 12m", + }, + { + id: "conn-2", + name: "Game TCP", + serverLabel: "Frankfurt", + protocol: "tcp", + localPort: 25565, + serverPort: 20555, + remote: "tcp://eu.tunnl.live:20555", + status: "connected", + latencyMs: 120, + dataInOut: "320 MB", + startedAgo: "54m", + }, + ]) + + type SessionResponse = Awaited> + const [session, setSession] = useState(null) + + useEffect(() => { + const fetchSession = async () => { + try { + const result = await authClient.getSession() + if (result.data) { + setSession(result.data) + } + } catch (error) { + console.error("Error fetching session", error) + } + } + + fetchSession() + }, []) + + const handleSignOut = async () => { + try { + await authClient.signOut() + setSession(null) + } catch (error) { + console.error("Error signing out", error) + } + } + + const stopConnection = (id: string) => { + setActiveConnections((prev) => prev.filter((conn) => conn.id !== id)) + setStatusMessage("Connection stopped") + } + + return ( +
+
+
+
+
+ + + + + + + + + + + + + + + + + + tunnl.live + +
+ +
+ {session?.user ? ( + + ) : ( + <> + + + + + + + Sign In + + +
+ + + + + + Sign in to save configurations & view history +
+ + )} +
+
+
+
+ +
+
+
+

Active Forwarding

+

Live tunnels for this session.

+
+ + {statusMessage && ( +
+ {statusMessage} +
+ )} + +
+
+
+

Active Connections

+

Monitor and manage your running tunnels

+
+ + View logs + +
+ + {activeConnections.length === 0 ? ( +
+ No active connections yet. Configure a tunnel to see it here. +
+ ) : ( +
+ {activeConnections.map((connection) => ( +
+
+
+ {connection.name} + + {connection.status === "connected" + ? "Connected" + : connection.status === "pending" + ? "Reconnecting" + : "Error"} + +
+

+ {connection.protocol.toUpperCase()} · {connection.serverLabel} +

+

{connection.remote}

+

+ Local {connection.localPort} → Server {connection.serverPort} · {connection.startedAgo} +

+
+ +
+
+

Latency

+

+ {connection.latencyMs ? `${connection.latencyMs}ms` : "—"} +

+
+
+

Data

+

{connection.dataInOut}

+
+ +
+
+ ))} +
+ )} +
+ +
+
+
+

Custom Tunnel Configuration

+

Pick a location, test latency, and shape your tunnel exactly how you need.

+
+ + View docs + +
+ + +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + tunnl.live +
+
+ © {new Date().getFullYear()} tunnl.live. Made with ❤️ by{" "} + + Bagas + . All rights reserved. +
+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index 79e9d86..17cccb4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -639,6 +639,7 @@ export default function Home() { selectedServer={selectedServer} onServerSelect={setSelectedServer} isAuthenticated={logedin != null ? true : false} + userId={logedin?.id} />
diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000..58039ff --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,882 @@ +"use client" + +import { useEffect, useState } from "react" +import Link from "next/link" +import UserMenu from "@/components/user-menu" +import { authClient } from "@/lib/auth-client" + +export default function SettingsPage() { + type SessionResponse = Awaited> + const [session, setSession] = useState(null) + const [requireAuth, setRequireAuth] = useState(true) + const [message, setMessage] = useState(null) + + useEffect(() => { + const loadSession = async () => { + try { + const result = await authClient.getSession() + if (result.data) { + setSession(result.data) + } + } catch (error) { + console.error("Failed to load session", error) + } + } + + loadSession() + }, []) + + const handleToggle = (value: boolean) => { + setRequireAuth(value) + setMessage(value ? "Authentication required for tunnel requests" : "Authentication not required for tunnel requests") + setTimeout(() => setMessage(null), 2500) + } + + return ( +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tunnl.live + +
+ +
+ {session?.user ? ( + { + try { + await authClient.signOut() + setSession(null) + } catch (error) { + console.error("Error signing out", error) + } + }} + /> + ) : ( + <> + + + + + + + Sign In + + +
+ + + + + + Sign in to save configurations & view history +
+ + )} +
+
+
+
+ +
+
+
+

Settings

+

Control how tunnels can be requested.

+
+ + {message && ( +
+ {message} +
+ )} + +
+
+
+

Tunnel Request Authentication

+

Require users to be authenticated before they can request a tunnel.

+
+ +
+

+ This toggle is local-only for now. Wire it to your backend when ready to enforce tunnel request policies. +

+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tunnl.live +
+
+ © {new Date().getFullYear()} tunnl.live. Made with ❤️ by{' '} + + Bagas + . All rights reserved. +
+ +
+
+
+ ) +} diff --git a/components/tunnel-config.tsx b/components/tunnel-config.tsx index d5ea961..63e3b42 100644 --- a/components/tunnel-config.tsx +++ b/components/tunnel-config.tsx @@ -38,6 +38,7 @@ interface TunnelConfigProps { selectedServer: Server | null onServerSelect: (server: Server) => void isAuthenticated?: boolean + userId?: string } const fetchServers = async (): Promise => { @@ -181,6 +182,7 @@ export default function TunnelConfig({ selectedServer, onServerSelect, isAuthenticated = false, + userId, }: TunnelConfigProps) { const [localConfig, setLocalConfig] = useState({ ...config, @@ -363,14 +365,14 @@ export default function TunnelConfig({ return } - // Navigate to custom domain configuration page window.location.href = "/custom-domain" } const generateCommand = () => { if (!selectedServer) return "" const { serverPort, localPort } = localConfig - return `ssh ${selectedServer.subdomain} -p 2200 -R ${serverPort}:localhost:${localPort}` + const target = isAuthenticated && userId ? `${userId}@${selectedServer.subdomain}` : selectedServer.subdomain + return `ssh ${target} -p 2200 -R ${serverPort}:localhost:${localPort}` } const copyToClipboard = () => { @@ -853,8 +855,8 @@ export default function TunnelConfig({ <>
-
-