From 018a980dcbc0ffd2cd433cec9d0b59ebf756b8fb Mon Sep 17 00:00:00 2001 From: bagas Date: Wed, 1 Oct 2025 17:16:58 +0700 Subject: [PATCH] feat: add auth --- .dockerignore | 3 +- app/api/auth/[...all]/route.ts | 3 + app/login/page.tsx | 157 +++ app/page.tsx | 610 ++++++++++- components/tunnel-config.tsx | 395 ++++--- components/user-menu.tsx | 150 +++ lib/auth-client.ts | 5 + lib/auth.ts | 17 + lib/db.ts | 3 + lib/schema/auth.ts | 61 ++ middleware.ts | 15 + package-lock.json | 1794 +++++++++++++++++++++++++++++++- package.json | 8 + 13 files changed, 3057 insertions(+), 164 deletions(-) create mode 100644 app/api/auth/[...all]/route.ts create mode 100644 app/login/page.tsx create mode 100644 components/user-menu.tsx create mode 100644 lib/auth-client.ts create mode 100644 lib/auth.ts create mode 100644 lib/db.ts create mode 100644 lib/schema/auth.ts create mode 100644 middleware.ts diff --git a/.dockerignore b/.dockerignore index 72e9aa4..170afd4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,5 @@ node_modules npm-debug.log README.md .next -.git \ No newline at end of file +.git +.env \ No newline at end of file diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..edfad17 --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,3 @@ +import { auth } from "@/lib/auth"; +import { toNextJsHandler } from "better-auth/next-js"; +export const { POST, GET } = toNextJsHandler(auth); \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..1a3c5f8 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,157 @@ +"use client" + +import { useState } from "react" +import Link from "next/link" +import { createAuthClient } from "better-auth/client"; + +const authClient = createAuthClient(); + +export default function LoginPage() { + const [isLoading, setIsLoading] = useState(false) + + const handleGoogleSignIn = async () => { + setIsLoading(true) + try { + await authClient.signIn.social({ + provider: "google", + }); + + console.log("Google sign-in clicked") + } catch (error) { + console.error("Sign-in error:", error) + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+
+ +

+ tunnl.live +

+ +

Sign in to manage your tunnels

+
+ +
+
+

Welcome Back

+

Sign in to access your tunnel dashboard

+
+ + + +
+
+
+
+
+ or +
+
+ + + + + + + + Continue as Guest + + +
+

Benefits of signing in:

+
    +
  • +
    + Save and manage your tunnel configurations +
  • +
  • +
    + View tunnel usage statistics and history +
  • +
  • +
    + Access to premium features and priority support +
  • +
  • +
    + Sync settings across multiple devices +
  • +
+
+
+ +
+

+ By signing in, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + +

+

+ Need help?{" "} + + Contact Support + +

+
+
+
+ +
+
+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index aeb7f38..79e9d86 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,11 @@ "use client" -import { useState } from "react" +import { useEffect, useState } from "react" import TunnelConfig, { type TunnelConfig as TunnelConfigType, type Server } from "@/components/tunnel-config" +import Link from "next/link" +import { authClient } from "@/lib/auth-client"; +import UserMenu from "@/components/user-menu" +import { redirect, RedirectType } from 'next/navigation' const defaultConfig: TunnelConfigType = { type: "http", @@ -12,9 +16,612 @@ const defaultConfig: TunnelConfigType = { export default function Home() { const [selectedServer, setSelectedServer] = useState(null) const [tunnelConfig, setTunnelConfig] = useState(defaultConfig) + type SessionData = Awaited>; + const [logedin, setLogedin] = useState(null) + + useEffect(() => { + const fetchData = async () => { + try { + const result = await authClient.getSession() + if (result.data != null) { + setLogedin(result.data.user); + } + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + fetchData(); + }, []); + + const logout = async() => { + await authClient.signOut({ + fetchOptions: { + onSuccess: () => { + redirect('/login', RedirectType.replace) + } + } + }) + } return (
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tunnl.live + +
+ +
+ {logedin != null ? ( + + ) : ( + <> + + + + + + + Sign In + + +
+ + + + + + Sign in to save configurations & view history +
+ + )} +
+
+
+
@@ -31,6 +638,7 @@ export default function Home() { onConfigChange={setTunnelConfig} selectedServer={selectedServer} onServerSelect={setSelectedServer} + isAuthenticated={logedin != null ? true : false} />
diff --git a/components/tunnel-config.tsx b/components/tunnel-config.tsx index 8c7be37..4278ad0 100644 --- a/components/tunnel-config.tsx +++ b/components/tunnel-config.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react" import { ComposableMap, Geographies, Geography, Marker } from "react-simple-maps" +import Link from "next/link" export interface TunnelConfig { type: "http" | "tcp" @@ -36,6 +37,7 @@ interface TunnelConfigProps { onConfigChange: (config: TunnelConfig) => void selectedServer: Server | null onServerSelect: (server: Server) => void + isAuthenticated?: boolean } const fetchServers = async (): Promise => { @@ -85,9 +87,10 @@ const fetchServers = async (): Promise => { }, portRestrictions: { allowedRanges: [ - { min: 10000, max: 50000 }, + { min: 8000, max: 8999 }, + { min: 9000, max: 9999 }, ], - blockedPorts: [22, 80, 443, 3306, 5432, 6379, 2200], + blockedPorts: [8080, 8443, 9000], supportsAutoAssign: true, }, }, @@ -105,10 +108,7 @@ const fetchServers = async (): Promise => { tcp: true, }, portRestrictions: { - allowedRanges: [ - { min: 10000, max: 50000 }, - ], - blockedPorts: [22, 80, 443, 3306, 5432, 6379, 2200], + blockedPorts: [22, 80, 443, 3306, 5432, 6379], supportsAutoAssign: true, }, }, @@ -198,7 +198,13 @@ const testServerPing = ( }) } -export default function TunnelConfig({ config, onConfigChange, selectedServer, onServerSelect }: TunnelConfigProps) { +export default function TunnelConfig({ + config, + onConfigChange, + selectedServer, + onServerSelect, + isAuthenticated = false, +}: TunnelConfigProps) { const [localConfig, setLocalConfig] = useState({ ...config, serverPort: config.type === "tcp" ? 0 : config.serverPort, @@ -211,6 +217,7 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o const [serverError, setServerError] = useState(null) const [portError, setPortError] = useState(null) const [pendingServerSelection, setPendingServerSelection] = useState(null) + const [showTcpLoginPrompt, setShowTcpLoginPrompt] = useState(false) useEffect(() => { const loadServers = async () => { @@ -283,8 +290,9 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o (s) => s.pingStatus === "success" && s.ping !== null && + s.capabilities.http && ((localConfig.type === "http" && s.capabilities.http) || - (localConfig.type === "tcp" && s.capabilities.tcp)), + (localConfig.type === "tcp" && s.capabilities.tcp && isAuthenticated)), ) if (compatibleServers.length > 0) { @@ -293,11 +301,9 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o ) setPendingServerSelection(bestServer) } else { - const successfulServers = testedServers.filter((s) => s.pingStatus === "success" && s.ping !== null) - if (successfulServers.length > 0) { - const bestServer = successfulServers.reduce((prev, current) => - prev.ping! < current.ping! ? prev : current, - ) + const httpServers = testedServers.filter((s) => s.pingStatus === "success" && s.capabilities.http) + if (httpServers.length > 0) { + const bestServer = httpServers.reduce((prev, current) => (prev.ping! < current.ping! ? prev : current)) setPendingServerSelection(bestServer) } else if (testedServers.length > 0) { setPendingServerSelection(testedServers[0]) @@ -312,24 +318,18 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o autoTestPings() } - }, [servers, isLoadingServers, hasAutoTested, localConfig.type]) + }, [servers, isLoadingServers, hasAutoTested, localConfig.type, isAuthenticated]) useEffect(() => { if (pendingServerSelection) { onServerSelect(pendingServerSelection) setPendingServerSelection(null) - if (localConfig.type === "tcp" && !pendingServerSelection.capabilities.tcp) { + if (localConfig.type === "tcp" && (!pendingServerSelection.capabilities.tcp || !isAuthenticated)) { updateConfig({ type: "http", serverPort: 443 }) } } - }, [pendingServerSelection, onServerSelect, localConfig.type]) - - useEffect(() => { - if (selectedServer && localConfig.type === "tcp" && !selectedServer.capabilities.tcp) { - updateConfig({ type: "http", serverPort: 443 }) - } - }, [selectedServer, localConfig.type]) + }, [pendingServerSelection, onServerSelect, localConfig.type, isAuthenticated]) useEffect(() => { if (selectedServer && localConfig.type === "tcp" && localConfig.serverPort !== 0) { @@ -375,6 +375,16 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o onConfigChange(newConfig) } + const handleTcpSelection = () => { + if (!isAuthenticated) { + setShowTcpLoginPrompt(true) + return + } + + updateConfig({ type: "tcp", serverPort: 0 }) + setShowTcpLoginPrompt(false) + } + const generateCommand = () => { if (!selectedServer) return "" const { serverPort, localPort } = localConfig @@ -520,7 +530,7 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o if (localConfig.type === "http" && !server.capabilities.http) { return false } - if (localConfig.type === "tcp" && !server.capabilities.tcp) { + if (localConfig.type === "tcp" && (!server.capabilities.tcp || !isAuthenticated)) { return false } @@ -534,6 +544,9 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o if (localConfig.type === "tcp" && !server.capabilities.tcp) { return "TCP not supported" } + if (localConfig.type === "tcp" && !isAuthenticated) { + return "Sign in required for TCP" + } if (localConfig.type === "http" && !server.capabilities.http) { return "HTTP not supported" } @@ -543,7 +556,7 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o const getCompatibleServers = () => { return servers.filter((server) => { if (localConfig.type === "http") return server.capabilities.http - if (localConfig.type === "tcp") return server.capabilities.tcp + if (localConfig.type === "tcp") return server.capabilities.tcp && isAuthenticated return true }) } @@ -562,6 +575,7 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o } const compatibleServers = getCompatibleServers() + const hasTcpServers = servers.some((s) => s.capabilities.tcp) return (
@@ -619,10 +633,13 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o

No servers support {localConfig.type.toUpperCase()} forwarding + {!isAuthenticated && localConfig.type === "tcp" && " for guest users"}

- Please switch to HTTP/HTTPS forwarding or wait for TCP-compatible servers to come online. + {!isAuthenticated && localConfig.type === "tcp" + ? "Please sign in to access TCP forwarding or switch to HTTP/HTTPS forwarding." + : "Please switch to HTTP/HTTPS forwarding or wait for TCP-compatible servers to come online."}

)} @@ -807,12 +824,18 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o HTTP )} {server.capabilities.tcp && ( - TCP + + TCP{!isAuthenticated && " 🔒"} + )}
- {server.capabilities.tcp && ( + {server.capabilities.tcp && isAuthenticated && (

{getPortRestrictionInfo(server)}

@@ -861,84 +884,88 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
-