Compare commits

..

1 Commits

Author SHA1 Message Date
1c6b05ad02 chore(deps): update react monorepo to v19.2.3 2025-12-30 08:28:38 +00:00
23 changed files with 1394 additions and 2173 deletions

View File

@@ -52,7 +52,7 @@ jobs:
context: . context: .
push: true push: true
tags: | tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please-frontend:latest git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnl_please_frontend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
@@ -62,6 +62,6 @@ jobs:
context: . context: .
push: true push: true
tags: | tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please-frontend:staging git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnl_please_frontend:staging
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
if: github.ref == 'refs/heads/staging' if: github.ref == 'refs/heads/staging'

View File

@@ -1,29 +1,28 @@
FROM node:24-slim AS base FROM node:22-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1 WORKDIR /app
FROM base AS deps
ENV NODE_ENV=development
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
RUN npm ci RUN npm ci
FROM base AS builder
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
RUN npm run build RUN npm run build
FROM gcr.io/distroless/nodejs24-debian13 AS runner FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
COPY --from=builder /app/.next/standalone ./ WORKDIR /app
COPY --from=builder /app/.next/static ./.next/static
# COPY --from=builder /app/public ./public COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
#COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
ENV NODE_ENV=production
EXPOSE 3000 EXPOSE 3000
CMD ["server.js"] CMD ["npm", "start"]

View File

@@ -1,60 +0,0 @@
import { auth } from "@/lib/auth";
import { headers } from "next/headers"
import { NextRequest } from "next/server";
export async function PATCH(req: NextRequest, context: { params: Promise<{ params: string[] }> }) {
const { params } = await context.params;
const requestHeaders = await headers();
const result = await auth.api.getToken({ headers: requestHeaders }).catch(() => null);
if (!result || typeof result !== "object" || !("token" in result)) {
return new Response("Unauthorized", { status: 401 });
}
const { token } = result as { token: string };
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
const data = await fetch(`${process.env.API_URL}/api/session/${params.join("/")}`, {
method: "PATCH",
headers: {
authorization: `Bearer ${token}`,
},
cache: "no-store",
body: await req.text(),
})
return data;
}
export async function DELETE(req: NextRequest, context: { params: Promise<{ params: string[] }> }) {
const { params } = await context.params;
if (!params || params.length < 3) {
return new Response("Bad Request", { status: 400 });
}
const node = params[0]
const tunnelType = params[1]
const slug = params[2]
const requestHeaders = await headers();
const result = await auth.api.getToken({ headers: requestHeaders }).catch(() => null);
if (!result || typeof result !== "object" || !("token" in result)) {
return new Response("Unauthorized", { status: 401 });
}
const { token } = result as { token: string };
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
const data = await fetch(`${process.env.API_URL}/api/session/${node}/${tunnelType}/${slug}`, {
method: "DELETE",
headers: {
authorization: `Bearer ${token}`,
},
cache: "no-store",
body: await req.text(),
})
return data;
}

View File

@@ -1,556 +0,0 @@
"use client"
import { useEffect, useState, type FormEvent } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import TunnelConfig, { type TunnelConfig as TunnelConfigType, type Server } from "@/components/tunnel-config"
import { authClient } from "@/lib/auth-client"
const defaultConfig: TunnelConfigType = {
type: "http",
serverPort: 443,
localPort: 8000,
}
const STOP_CONFIRMATION_TEXT = "YEAH SURE BUDDY"
const formatStartedAgo = (timestamp?: ApiTimestamp): string | undefined => {
if (!timestamp) return undefined
const startedMs = timestamp.seconds * 1000 + Math.floor(timestamp.nanos / 1_000_000)
const diffSeconds = Math.max(0, Math.floor((Date.now() - startedMs) / 1000))
if (diffSeconds < 60) return `${diffSeconds}s ago`
const diffMinutes = Math.floor(diffSeconds / 60)
if (diffMinutes < 60) return `${diffMinutes}m ago`
const diffHours = Math.floor(diffMinutes / 60)
if (diffHours < 24) return `${diffHours}h ago`
const diffDays = Math.floor(diffHours / 24)
return `${diffDays}d ago`
}
const toActiveConnection = (session: ApiSession): ActiveConnection => {
const startedAgo = formatStartedAgo(session.started_at)
return {
id: session.slug || `${session.node}-${session.started_at?.seconds ?? Date.now()}`,
name: session.slug || session.node || "Unknown tunnel",
slug: session.slug,
status: session.active ? "connected" : "error",
protocol: (session.forwarding_type || "http").toLowerCase(),
serverLabel: session.node || "Unknown node",
node: session.node,
remote: session.slug ? `${session.slug}.${session.node}` : session.node || "—",
startedAgo,
}
}
type ApiTimestamp = {
seconds: number
nanos: number
}
type ApiSession = {
node: string
forwarding_type: "HTTP" | "HTTPS" | "TCP" | string
slug: string
user_id: string
active: boolean
started_at?: ApiTimestamp
}
type ApiSessionList = ApiSession[]
type SessionResponse = Awaited<ReturnType<typeof authClient.getSession>>
interface DashboardClientProps {
initialActiveConnections: ApiSessionList
}
type ActiveConnectionStatus = "connected" | "pending" | "error"
type ActiveConnection = {
id: string
name: string
slug?: string
status: ActiveConnectionStatus
protocol: string
serverLabel: string
node?: string
remote: string
localPort?: number
serverPort?: number
startedAgo?: string
}
export default function DashboardClient({ initialActiveConnections }: DashboardClientProps) {
const router = useRouter()
const [selectedServer, setSelectedServer] = useState<Server | null>(null)
const [tunnelConfig, setTunnelConfig] = useState<TunnelConfigType>(defaultConfig)
const [statusMessage, setStatusMessage] = useState<string | null>(null)
const [activeConnections, setActiveConnections] = useState<ActiveConnection[]>(
initialActiveConnections.map(toActiveConnection),
)
const { data: cachedSession } = authClient.useSession()
const [session, setSession] = useState<SessionResponse["data"] | null>(cachedSession ?? null)
const [openActionId, setOpenActionId] = useState<string | null>(null)
const [stopModal, setStopModal] = useState<{
connectionId: string
node?: string
slug?: string
protocol?: string
name: string
} | null>(null)
const [stopModalInput, setStopModalInput] = useState("")
const [stopSaving, setStopSaving] = useState(false)
const [stopError, setStopError] = useState<string | null>(null)
const [slugModal, setSlugModal] = useState<{
connectionId: string
currentSlug: string
newSlug: string
node: string
} | null>(null)
const [slugError, setSlugError] = useState<string | null>(null)
const [slugSaving, setSlugSaving] = useState(false)
useEffect(() => {
setActiveConnections(initialActiveConnections.map(toActiveConnection))
}, [initialActiveConnections])
useEffect(() => {
if (!session && cachedSession) {
setSession(cachedSession)
}
}, [cachedSession, session])
const openStopModal = (connection: ActiveConnection) => {
setStopModal({
connectionId: connection.id,
node: connection.node || connection.serverLabel,
slug: connection.slug || connection.name,
protocol: connection.protocol,
name: connection.name,
})
setStopModalInput("")
setStopError(null)
setOpenActionId(null)
}
const closeStopModal = () => {
setStopModal(null)
setStopModalInput("")
setStopSaving(false)
setStopError(null)
}
const stopConnection = async () => {
if (!stopModal) return
if (stopModalInput.trim() !== STOP_CONFIRMATION_TEXT) {
setStopError("Please type the confirmation phrase exactly.")
return
}
const node = stopModal.node
const tunnelType = stopModal.protocol?.toLowerCase()
const slug = stopModal.slug
if (!node || !tunnelType || !slug) {
setStopError("Missing connection details; cannot stop.")
return
}
setStopSaving(true)
setStopError(null)
setStatusMessage(null)
try {
const endpoint = `/api/session/${encodeURIComponent(node)}/${encodeURIComponent(tunnelType)}/${encodeURIComponent(slug)}`
const response = await fetch(endpoint, {
method: "DELETE",
})
if (!response.ok) {
const message = await response.text()
setStopError(message || "Failed to stop connection.")
return
}
setActiveConnections((prev) => prev.filter((conn) => conn.id !== stopModal.connectionId))
setStatusMessage("Connection stopped")
closeStopModal()
} catch (error) {
console.error("Failed to stop connection", error)
setStopError("Failed to stop connection.")
} finally {
setStopSaving(false)
}
}
const openChangeSlugModal = (connection: ActiveConnection) => {
setSlugModal({
connectionId: connection.id,
currentSlug: connection.name,
newSlug: connection.name,
node: connection.node || connection.serverLabel,
})
setSlugError(null)
setOpenActionId(null)
}
const closeSlugModal = () => setSlugModal(null)
const validateSlug = (value: string): string | null => {
const trimmed = value.trim().toLowerCase()
if (trimmed.length < 3 || trimmed.length > 20) return "Slug must be 3-20 characters."
if (!/^[a-z0-9-]+$/.test(trimmed)) return "Only lowercase letters, numbers, and hyphens are allowed."
if (trimmed.startsWith("-") || trimmed.endsWith("-")) return "No leading or trailing hyphens."
return null
}
const submitSlugChange = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (!slugModal) return
const trimmedSlug = slugModal.newSlug.trim().toLowerCase()
const validationError = validateSlug(trimmedSlug)
setSlugError(validationError)
if (validationError) return
setSlugSaving(true)
setStatusMessage(null)
try {
const response = await fetch(`/api/session/${slugModal.node}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ old: slugModal.currentSlug, new: trimmedSlug }),
})
if (!response.ok) {
const message = await response.text()
setSlugError(message || "Failed to update slug.")
return
}
setActiveConnections((prev) =>
prev.map((conn) =>
conn.id === slugModal.connectionId
? {
...conn,
name: trimmedSlug,
remote: conn.node ? `${trimmedSlug}.${conn.node}` : trimmedSlug,
}
: conn,
),
)
setStatusMessage("Slug updated")
setSlugModal(null)
} catch (error) {
console.error("Failed to update slug", error)
setSlugError("Failed to update slug.")
} finally {
setSlugSaving(false)
}
}
useEffect(() => {
if (slugModal || stopModal) {
const previousOverflow = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => {
document.body.style.overflow = previousOverflow
}
}
}, [slugModal, stopModal])
useEffect(() => {
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpenActionId(null)
setSlugModal(null)
setStopModal(null)
}
}
window.addEventListener("keydown", closeOnEscape)
return () => window.removeEventListener("keydown", closeOnEscape)
}, [])
const stopModalContent = !stopModal
? null
: (
<div className="fixed inset-0 z-30 flex items-center justify-center bg-black/60 px-4">
<div className="w-full max-w-md rounded-lg border border-gray-800 bg-gray-900 p-6 shadow-xl">
<div className="flex items-start justify-between gap-4 mb-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-red-500/10 ring-1 ring-red-500/20">
<svg className="h-5 w-5 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-white">Stop connection</h3>
<p className="mt-1 text-sm text-gray-400 leading-relaxed">
Type <span className="font-mono text-xs text-red-300 bg-red-950/50 px-1.5 py-0.5 rounded">{STOP_CONFIRMATION_TEXT}</span> to stop <span className="font-medium text-gray-300">{stopModal.slug || stopModal.name}</span> on {stopModal.node || "this node"}.
</p>
</div>
</div>
<button
onClick={closeStopModal}
className="flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-800 hover:text-gray-200"
aria-label="Close modal"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="mt-4 space-y-4">
<label className="block text-sm text-gray-300">
Confirmation phrase
<input
type="text"
value={stopModalInput}
onChange={(e) => {
setStopModalInput(e.target.value)
if (stopError) setStopError(null)
}}
placeholder={STOP_CONFIRMATION_TEXT}
className="mt-2 w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-white focus:border-emerald-500 focus:outline-none"
/>
</label>
{stopError && <p className="text-sm text-red-400">{stopError}</p>}
<div className="flex items-center justify-end gap-3">
<button
type="button"
onClick={closeStopModal}
className="rounded-md px-4 py-2 text-sm font-medium bg-gray-700 text-gray-200 border border-gray-700 hover:bg-gray-600 hover:border-gray-500"
disabled={stopSaving}
>
Cancel
</button>
<button
type="button"
onClick={stopConnection}
disabled={stopModalInput.trim() !== STOP_CONFIRMATION_TEXT || stopSaving}
className={`rounded-md px-4 py-2 text-sm font-medium text-white${stopModalInput.trim() === STOP_CONFIRMATION_TEXT && !stopSaving ? 'bg-red-600 hover:bg-red-700 border border-gray-500' : 'bg-red-900 hover:text-white-300 cursor-not-allowed border border-gray-700'}`}>
{stopSaving ? "Stopping..." : "Confirm stop"}
</button>
</div>
</div>
</div>
</div>
)
const slugModalContent = !slugModal
? null
: (
<div className="fixed inset-0 z-20 flex items-center justify-center bg-black/60 px-4">
<div className="w-full max-w-md rounded-lg border border-gray-800 bg-gray-900 p-6 shadow-xl">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-white">Change slug</h3>
<p className="text-sm text-gray-400">Update the identifier for this tunnel.</p>
</div>
<button
onClick={closeSlugModal}
className="text-gray-400 hover:text-gray-200"
aria-label="Close modal"
>
×
</button>
</div>
<form onSubmit={submitSlugChange} className="mt-4 space-y-4">
<label className="block text-sm text-gray-300">
New slug
<input
type="text"
value={slugModal.newSlug}
onChange={(e) => {
const nextValue = e.target.value.toLowerCase()
setSlugModal((prev) => (prev ? { ...prev, newSlug: nextValue } : prev))
setSlugError(validateSlug(nextValue))
}}
className="mt-2 w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-white focus:border-emerald-500 focus:outline-none"
placeholder={slugModal.currentSlug}
/>
{slugError && <p className="mt-2 text-sm text-red-400">{slugError}</p>}
</label>
<div className="flex items-center justify-end gap-3">
<button
type="button"
onClick={closeSlugModal}
className="rounded-md border border-gray-700 px-4 py-2 text-sm text-gray-200 hover:border-gray-500"
>
Cancel
</button>
<button
type="submit"
disabled={Boolean(slugError) || slugSaving}
className="rounded-md bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-500 disabled:cursor-not-allowed disabled:opacity-60"
>
{slugSaving ? "Saving..." : "Save slug"}
</button>
</div>
</form>
</div>
</div>
)
return (
<>
<main className="flex-1">
<div className="max-w-7xl mx-auto px-4 py-8 space-y-6">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold">Active Forwarding</h1>
<p className="text-sm text-gray-400">Live tunnels for this session.</p>
</div>
{statusMessage && (
<div className="rounded-lg border border-emerald-700 bg-emerald-900/40 px-4 py-3 text-sm text-emerald-200">
{statusMessage}
</div>
)}
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold">Active Connections</h2>
<p className="text-sm text-gray-400">Monitor and manage your running tunnels</p>
</div>
<button
type="button"
onClick={() => router.refresh()}
className="text-sm text-emerald-400 hover:text-emerald-300"
>
Refresh
</button>
</div>
{activeConnections.length === 0 ? (
<div className="rounded-lg border border-dashed border-gray-700 bg-gray-800/60 p-6 text-center text-gray-400">
No active connections yet. Configure a tunnel to see it here.
</div>
) : (
<div className="space-y-3">
{activeConnections.map((connection) => {
const metaParts: string[] = []
if (connection.localPort && connection.serverPort) {
metaParts.push(`Local ${connection.localPort} → Server ${connection.serverPort}`)
}
if (connection.startedAgo) {
metaParts.push(connection.startedAgo)
}
const metaText = metaParts.length > 0 ? metaParts.join(" · ") : "No session metadata yet"
return (
<div
key={connection.id}
className="rounded-lg border border-gray-800 bg-gray-800/60 p-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-white font-medium">{connection.name}</span>
<span
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${connection.status === "connected"
? "bg-emerald-900/60 text-emerald-300 border border-emerald-700"
: connection.status === "pending"
? "bg-yellow-900/60 text-yellow-300 border border-yellow-700"
: "bg-red-900/60 text-red-300 border border-red-700"
}`}
>
{connection.status === "connected"
? "Connected"
: connection.status === "pending"
? "Reconnecting"
: "Error"}
</span>
</div>
<p className="text-sm text-gray-300">
{(connection.protocol || "http").toUpperCase()} · {connection.serverLabel}
</p>
{(() => {
const isTcp = connection.protocol === "tcp"
const isHttp = connection.protocol === "http" || connection.protocol === "https"
const httpRemote = connection.remote ? `https://${connection.remote}` : "—"
const tcpRemote =
connection.node && connection.name
? `tcp://${connection.node}:${connection.name}`
: connection.remote || "—"
const displayRemote = isTcp ? tcpRemote : isHttp ? httpRemote : connection.remote || "—"
return <p className="text-xs text-gray-400">{displayRemote}</p>
})()}
<p className="text-xs text-gray-500">{metaText}</p>
</div>
<div className="flex flex-wrap items-center gap-3 md:justify-end relative">
<button
onClick={() =>
setOpenActionId((current) => (current === connection.id ? null : connection.id))
}
className="rounded-lg border border-gray-700 px-3 py-2 text-sm text-gray-200 hover:border-gray-500 transition"
>
Actions
</button>
{openActionId === connection.id && (
<div className="absolute right-0 top-12 z-10 w-44 rounded-md border border-gray-700 bg-gray-800 shadow-lg">
<button
className="w-full px-3 py-2 text-left text-sm text-gray-200 hover:bg-gray-700"
onClick={() => openStopModal(connection)}
>
Stop connection
</button>
{connection.protocol !== "tcp" && (
<button
className="w-full px-3 py-2 text-left text-sm text-gray-200 hover:bg-gray-700"
onClick={() => openChangeSlugModal(connection)}
>
Change slug
</button>
)}
</div>
)}
</div>
</div>
)
})}
</div>
)}
</div>
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
<div className="flex flex-col gap-2 mb-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold">Custom Tunnel Configuration</h2>
<p className="text-sm text-gray-400">Pick a location, test latency, and shape your tunnel exactly how you need.</p>
</div>
<Link href="/tunnel-not-found" className="text-sm text-emerald-400 hover:text-emerald-300">
View docs
</Link>
</div>
<TunnelConfig
config={tunnelConfig}
onConfigChange={setTunnelConfig}
selectedServer={selectedServer}
onServerSelect={setSelectedServer}
isAuthenticated={Boolean(session)}
userId={session?.user?.sshIdentifier}
/>
</div>
</div>
</main>
{stopModalContent}
{slugModalContent}
</>
)
}

View File

@@ -1,40 +0,0 @@
import SiteHeader from "@/components/site-header"
import SiteFooter from "@/components/site-footer"
import { auth } from "@/lib/auth"
import { headers } from "next/headers"
import DashboardClient from "./dashboard-client"
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const requestHeaders = await headers()
const session = await auth.api.getSession({
headers: requestHeaders,
}).catch(() => {
redirect('/')
})
const { token } = await auth.api.getToken({
headers: requestHeaders,
}).catch(() => {
redirect('/')
})
const data = await fetch(`${process.env.API_URL}/api/sessions`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
},
cache: "no-store",
})
const initialActiveConnections = await data.json()
return (
<div className="flex min-h-screen flex-col bg-gray-950 text-white">
<SiteHeader session={session} />
<DashboardClient
initialActiveConnections={initialActiveConnections}
/>
<SiteFooter />
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +0,0 @@
"use client"
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth-client"
import SiteHeader from "@/components/site-header"
import SiteFooter from "@/components/site-footer"
export default function SettingsPage() {
const [requireAuth, setRequireAuth] = useState(true)
const [message, setMessage] = useState<string | null>(null)
const { data: session, isPending } = authClient.useSession();
const router = useRouter();
useEffect(() => {
if (!isPending && !session) {
router.push('/login');
}
}, [session, isPending, router]);
if (isPending) {
return <div>Loading...</div>;
}
const handleToggle = (value: boolean) => {
setRequireAuth(value)
setMessage(value ? "Authentication required for tunnel requests" : "Authentication not required for tunnel requests")
setTimeout(() => setMessage(null), 2500)
}
return session ? (
<div className="flex min-h-screen flex-col bg-gray-950 text-white">
<SiteHeader session={session} />
<main className="flex-1">
<div className="max-w-5xl mx-auto px-4 py-8 space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Settings</h1>
<p className="text-sm text-gray-400">Control how tunnels can be requested.</p>
</div>
{message && (
<div className="rounded-lg border border-emerald-700 bg-emerald-900/40 px-4 py-3 text-sm text-emerald-200">
{message}
</div>
)}
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5 space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold">Tunnel Request Authentication</h2>
<p className="text-sm text-gray-400">Require users to be authenticated before they can request a tunnel.</p>
</div>
<label className="inline-flex items-center gap-2 cursor-pointer select-none">
<span className="text-sm text-gray-300">{requireAuth ? "Required" : "Not required"}</span>
<input
type="checkbox"
checked={requireAuth}
onChange={(e) => handleToggle(e.target.checked)}
className="sr-only"
/>
<span
className={`relative inline-flex h-6 w-11 items-center rounded-full border ${requireAuth ? "bg-emerald-600 border-emerald-500" : "bg-gray-700 border-gray-600"}`}
>
<span
className={`inline-block h-5 w-5 rounded-full bg-white shadow transition-transform ${requireAuth ? "translate-x-5" : "translate-x-1"}`}
/>
</span>
</label>
</div>
<p className="text-xs text-gray-500">
This toggle is local-only for now. Wire it to your backend when ready to enforce tunnel request policies.
</p>
</div>
</div>
</main>
<SiteFooter />
</div>
) : null;
}

View File

@@ -1,35 +0,0 @@
"use client"
import TunnlLogo from "./tunnl-logo"
import Link from "next/link"
export default function SiteFooter() {
return (
<footer className="border-t border-gray-800 py-6 px-4">
<div className="max-w-3xl mx-auto text-center">
<div className="flex items-center justify-center gap-2 mb-4">
<TunnlLogo />
<span className="text-xl font-bold">
<span className="text-emerald-400">tunnl</span>.live
</span>
</div>
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 mb-4 text-sm">
<Link href="https://status.fossy.my.id/status/services" className="text-gray-400 hover:text-emerald-400 transition-colors">
Status Page
</Link>
</div>
<div className="text-sm text-gray-500">
© {new Date().getFullYear()} tunnl.live. Made with by{' '}
<a
href="https://github.com/fossyy"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-gray-700"
>
Bagas
</a>. All rights reserved.
</div>
</div>
</footer>
)
}

View File

@@ -1,96 +0,0 @@
"use client"
import Link from "next/link"
import TunnlLogo from "./tunnl-logo"
import UserMenu from "./user-menu"
import { authClient } from "@/lib/auth-client";
import { redirect, RedirectType } from 'next/navigation'
type UseSessionReturn = ReturnType<typeof authClient.useSession>;
type SessionType = UseSessionReturn extends { data: infer D } ? D : never;
type SiteHeaderProps = {
session?: SessionType;
};
export default function SiteHeader({ session }: SiteHeaderProps) {
const logout = async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
redirect('/login', RedirectType.replace)
}
}
})
}
return (
<header className="border-b border-gray-800 bg-gray-900/50 backdrop-blur-sm">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TunnlLogo />
<span className="text-xl font-bold">
<span className="text-emerald-400">tunnl</span>.live
</span>
</div>
<div className="flex items-center gap-4">
{session ? (
<UserMenu
user={{
name: session.user?.name ?? "User",
email: session.user.email ?? "",
image: session.user.image ?? undefined,
}}
onSignOut={logout}
/>
) : (
<>
<Link
href="/login"
className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" x2="3" y1="12" y2="12" />
</svg>
Sign In
</Link>
<div className="hidden md:flex items-center gap-2 text-sm text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-emerald-400"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
<span>Sign in to save configurations & view history</span>
</div>
</>
)}
</div>
</div>
</div>
</header>
)
}

View File

@@ -5,7 +5,7 @@ import { ComposableMap, Geographies, Geography, Marker } from "react-simple-maps
import Link from "next/link" import Link from "next/link"
export interface TunnelConfig { export interface TunnelConfig {
type: "http" | "tcp" | "custom-domain" type: "http" | "tcp"
serverPort: number serverPort: number
localPort: number localPort: number
} }
@@ -38,7 +38,6 @@ interface TunnelConfigProps {
selectedServer: Server | null selectedServer: Server | null
onServerSelect: (server: Server) => void onServerSelect: (server: Server) => void
isAuthenticated?: boolean isAuthenticated?: boolean
userId?: string
} }
const fetchServers = async (): Promise<Server[]> => { const fetchServers = async (): Promise<Server[]> => {
@@ -56,35 +55,7 @@ const fetchServers = async (): Promise<Server[]> => {
pingStatus: "idle", pingStatus: "idle",
capabilities: { capabilities: {
http: true, http: true,
tcp: true, tcp: false,
},
portRestrictions: {
allowedRanges: [
{ min: 20000, max: 21000 },
],
blockedPorts: [8080, 8443, 9000],
supportsAutoAssign: true,
},
},
{
id: "fra",
name: "Frankfurt",
location: "Germany",
subdomain: "eu.tunnl.live",
coordinates: [8.6821, 50.1109],
ping: null,
status: "online",
pingStatus: "idle",
capabilities: {
http: true,
tcp: true,
},
portRestrictions: {
allowedRanges: [
{ min: 20000, max: 21000 },
],
blockedPorts: [8080, 8443, 9000],
supportsAutoAssign: true,
}, },
}, },
{ {
@@ -102,7 +73,8 @@ const fetchServers = async (): Promise<Server[]> => {
}, },
portRestrictions: { portRestrictions: {
allowedRanges: [ allowedRanges: [
{ min: 20000, max: 21000 }, { min: 8000, max: 8999 },
{ min: 9000, max: 9999 },
], ],
blockedPorts: [8080, 8443, 9000], blockedPorts: [8080, 8443, 9000],
supportsAutoAssign: true, supportsAutoAssign: true,
@@ -182,7 +154,6 @@ export default function TunnelConfig({
selectedServer, selectedServer,
onServerSelect, onServerSelect,
isAuthenticated = false, isAuthenticated = false,
userId,
}: TunnelConfigProps) { }: TunnelConfigProps) {
const [localConfig, setLocalConfig] = useState<TunnelConfig>({ const [localConfig, setLocalConfig] = useState<TunnelConfig>({
...config, ...config,
@@ -196,7 +167,7 @@ export default function TunnelConfig({
const [serverError, setServerError] = useState<string | null>(null) const [serverError, setServerError] = useState<string | null>(null)
const [portError, setPortError] = useState<string | null>(null) const [portError, setPortError] = useState<string | null>(null)
const [pendingServerSelection, setPendingServerSelection] = useState<Server | null>(null) const [pendingServerSelection, setPendingServerSelection] = useState<Server | null>(null)
const [showCustomDomainLoginPrompt, setShowCustomDomainLoginPrompt] = useState(false) const [showTcpLoginPrompt, setShowTcpLoginPrompt] = useState(false)
useEffect(() => { useEffect(() => {
const loadServers = async () => { const loadServers = async () => {
@@ -271,7 +242,7 @@ export default function TunnelConfig({
s.ping !== null && s.ping !== null &&
s.capabilities.http && s.capabilities.http &&
((localConfig.type === "http" && 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) { if (compatibleServers.length > 0) {
@@ -304,7 +275,7 @@ export default function TunnelConfig({
onServerSelect(pendingServerSelection) onServerSelect(pendingServerSelection)
setPendingServerSelection(null) setPendingServerSelection(null)
if (localConfig.type === "tcp" && !pendingServerSelection.capabilities.tcp) { if (localConfig.type === "tcp" && (!pendingServerSelection.capabilities.tcp || !isAuthenticated)) {
updateConfig({ type: "http", serverPort: 443 }) updateConfig({ type: "http", serverPort: 443 })
} }
} }
@@ -355,24 +326,19 @@ export default function TunnelConfig({
} }
const handleTcpSelection = () => { const handleTcpSelection = () => {
setShowCustomDomainLoginPrompt(false)
updateConfig({ type: "tcp", serverPort: 0 })
}
const handleCustomDomainSelection = () => {
if (!isAuthenticated) { if (!isAuthenticated) {
setShowCustomDomainLoginPrompt(true) setShowTcpLoginPrompt(true)
return return
} }
window.location.href = "/custom-domain" updateConfig({ type: "tcp", serverPort: 0 })
setShowTcpLoginPrompt(false)
} }
const generateCommand = () => { const generateCommand = () => {
if (!selectedServer) return "" if (!selectedServer) return ""
const { serverPort, localPort } = localConfig const { serverPort, localPort } = localConfig
const target = isAuthenticated && userId ? `${userId}@${selectedServer.subdomain}` : selectedServer.subdomain return `ssh ${selectedServer.subdomain} -p 2200 -R ${serverPort}:localhost:${localPort}`
return `ssh ${target} -p 2200 -R ${serverPort}:localhost:${localPort}`
} }
const copyToClipboard = () => { const copyToClipboard = () => {
@@ -514,7 +480,7 @@ export default function TunnelConfig({
if (localConfig.type === "http" && !server.capabilities.http) { if (localConfig.type === "http" && !server.capabilities.http) {
return false return false
} }
if (localConfig.type === "tcp" && !server.capabilities.tcp) { if (localConfig.type === "tcp" && (!server.capabilities.tcp || !isAuthenticated)) {
return false return false
} }
@@ -528,6 +494,9 @@ export default function TunnelConfig({
if (localConfig.type === "tcp" && !server.capabilities.tcp) { if (localConfig.type === "tcp" && !server.capabilities.tcp) {
return "TCP not supported" return "TCP not supported"
} }
if (localConfig.type === "tcp" && !isAuthenticated) {
return "Sign in required for TCP"
}
if (localConfig.type === "http" && !server.capabilities.http) { if (localConfig.type === "http" && !server.capabilities.http) {
return "HTTP not supported" return "HTTP not supported"
} }
@@ -537,7 +506,7 @@ export default function TunnelConfig({
const getCompatibleServers = () => { const getCompatibleServers = () => {
return servers.filter((server) => { return servers.filter((server) => {
if (localConfig.type === "http") return server.capabilities.http 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 return true
}) })
} }
@@ -614,10 +583,13 @@ export default function TunnelConfig({
</svg> </svg>
<p className="text-yellow-400 font-medium"> <p className="text-yellow-400 font-medium">
No servers support {localConfig.type.toUpperCase()} forwarding No servers support {localConfig.type.toUpperCase()} forwarding
{!isAuthenticated && localConfig.type === "tcp" && " for guest users"}
</p> </p>
</div> </div>
<p className="text-yellow-300 text-sm"> <p className="text-yellow-300 text-sm">
Please switch to HTTP/HTTPS forwarding or wait for 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."}
</p> </p>
</div> </div>
)} )}
@@ -764,7 +736,8 @@ export default function TunnelConfig({
onServerSelect(server) onServerSelect(server)
} }
}} }}
className={`p-3 rounded-lg border transition-all duration-200 ${selectedServer?.id === server.id className={`p-3 rounded-lg border transition-all duration-200 ${
selectedServer?.id === server.id
? "bg-emerald-950 border-emerald-500" ? "bg-emerald-950 border-emerald-500"
: !canSelect : !canSelect
? "bg-red-950 border-red-800 cursor-not-allowed opacity-75" ? "bg-red-950 border-red-800 cursor-not-allowed opacity-75"
@@ -774,7 +747,8 @@ export default function TunnelConfig({
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<h5 className="font-medium text-sm">{server.name}</h5> <h5 className="font-medium text-sm">{server.name}</h5>
<div <div
className={`w-2 h-2 rounded-full ${selectedServer?.id === server.id className={`w-2 h-2 rounded-full ${
selectedServer?.id === server.id
? "bg-emerald-400" ? "bg-emerald-400"
: !canSelect : !canSelect
? "bg-red-400" ? "bg-red-400"
@@ -800,14 +774,18 @@ export default function TunnelConfig({
<span className="text-xs bg-blue-900 text-blue-300 px-1.5 py-0.5 rounded">HTTP</span> <span className="text-xs bg-blue-900 text-blue-300 px-1.5 py-0.5 rounded">HTTP</span>
)} )}
{server.capabilities.tcp && ( {server.capabilities.tcp && (
<span className="text-xs bg-purple-900 text-purple-300 px-1.5 py-0.5 rounded"> <span
TCP className={`text-xs px-1.5 py-0.5 rounded ${
isAuthenticated ? "bg-purple-900 text-purple-300" : "bg-gray-700 text-gray-400"
}`}
>
TCP{!isAuthenticated && " 🔒"}
</span> </span>
)} )}
</div> </div>
</div> </div>
{server.capabilities.tcp && ( {server.capabilities.tcp && isAuthenticated && (
<div className="mb-2"> <div className="mb-2">
<p className="text-xs text-gray-300">{getPortRestrictionInfo(server)}</p> <p className="text-xs text-gray-300">{getPortRestrictionInfo(server)}</p>
</div> </div>
@@ -855,21 +833,19 @@ export default function TunnelConfig({
<> <>
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium mb-3">Forwarding Type</label> <label className="block text-sm font-medium mb-3">Forwarding Type</label>
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap"> <div className="flex gap-4">
<label className="flex w-full items-center cursor-pointer sm:w-auto"> <label className="flex items-center cursor-pointer">
<input <input
type="radio" type="radio"
name="forwardingType" name="forwardingType"
value="http" value="http"
checked={localConfig.type === "http"} checked={localConfig.type === "http"}
onChange={() => { onChange={() => updateConfig({ type: "http", serverPort: 443 })}
setShowCustomDomainLoginPrompt(false)
updateConfig({ type: "http", serverPort: 443 })
}}
className="sr-only" className="sr-only"
/> />
<div <div
className={`flex w-full items-center gap-2 rounded-lg border px-4 py-2 transition-all sm:w-auto ${localConfig.type === "http" className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${
localConfig.type === "http"
? "bg-emerald-950 border-emerald-500 text-emerald-400" ? "bg-emerald-950 border-emerald-500 text-emerald-400"
: "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600" : "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
}`} }`}
@@ -881,50 +857,32 @@ export default function TunnelConfig({
</div> </div>
</label> </label>
<label className="flex w-full items-center cursor-pointer sm:w-auto"> <label className="flex items-center cursor-pointer">
<input <input
type="radio" type="radio"
name="forwardingType" name="forwardingType"
value="tcp" value="tcp"
checked={localConfig.type === "tcp"} checked={localConfig.type === "tcp"}
onChange={handleTcpSelection} onChange={handleTcpSelection}
disabled={!isAuthenticated && !hasTcpServers}
className="sr-only" className="sr-only"
/> />
<div <div
className={`flex w-full items-center gap-2 rounded-lg border px-4 py-2 transition-all sm:w-auto ${localConfig.type === "tcp" className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${
localConfig.type === "tcp"
? "bg-emerald-950 border-emerald-500 text-emerald-400" ? "bg-emerald-950 border-emerald-500 text-emerald-400"
: "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600" : !isAuthenticated && hasTcpServers
? "bg-gray-800 border-gray-700 text-gray-400 cursor-pointer hover:border-yellow-600"
: isAuthenticated && hasTcpServers
? "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
: "bg-gray-800 border-gray-700 text-gray-500 cursor-not-allowed opacity-50"
}`} }`}
> >
<div <div
className={`w-2 h-2 rounded-full ${localConfig.type === "tcp" ? "bg-emerald-400" : "bg-gray-500"}`} className={`w-2 h-2 rounded-full ${localConfig.type === "tcp" ? "bg-emerald-400" : "bg-gray-500"}`}
/> />
<span className="font-medium">TCP</span> <span className="font-medium">TCP</span>
</div> {!isAuthenticated && hasTcpServers && (
</label>
<label className="flex w-full items-center cursor-pointer sm:w-auto">
<input
type="radio"
name="forwardingType"
value="custom-domain"
checked={localConfig.type === "custom-domain"}
onChange={handleCustomDomainSelection}
className="sr-only"
/>
<div
className={`flex w-full items-center gap-2 rounded-lg border px-4 py-2 transition-all sm:w-auto ${localConfig.type === "custom-domain"
? "bg-emerald-950 border-emerald-500 text-emerald-400"
: !isAuthenticated
? "bg-gray-800 border-gray-700 text-gray-400 cursor-pointer hover:border-yellow-600"
: "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
}`}
>
<div
className={`w-2 h-2 rounded-full ${localConfig.type === "custom-domain" ? "bg-emerald-400" : "bg-gray-500"}`}
/>
<span className="font-medium">Custom Domain</span>
{!isAuthenticated && (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="14" width="14"
@@ -941,7 +899,7 @@ export default function TunnelConfig({
<path d="M7 11V7a5 5 0 0 1 10 0v4" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg> </svg>
)} )}
{isAuthenticated && ( {isAuthenticated && hasTcpServers && (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="14" width="14"
@@ -966,14 +924,12 @@ export default function TunnelConfig({
<p className="text-sm text-gray-400 mt-2"> <p className="text-sm text-gray-400 mt-2">
{localConfig.type === "http" {localConfig.type === "http"
? "Best for web applications and APIs. Uses HTTPS (port 443) or HTTP (port 80)." ? "Best for web applications and APIs. Uses HTTPS (port 443) or HTTP (port 80)."
: localConfig.type === "tcp" : isAuthenticated
? "For any TCP service like databases, game servers, or custom applications." ? "For any TCP service like databases, game servers, or custom applications."
: !isAuthenticated : "TCP forwarding requires authentication for security and abuse prevention."}
? "Use your own domain name for professional tunneling. Requires authentication."
: "Configure and use your own custom domain for tunneling."}
</p> </p>
{showCustomDomainLoginPrompt && !isAuthenticated && ( {showTcpLoginPrompt && !isAuthenticated && (
<div className="mt-4 p-4 bg-blue-950 rounded-lg border border-blue-800"> <div className="mt-4 p-4 bg-blue-950 rounded-lg border border-blue-800">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<svg <svg
@@ -988,19 +944,20 @@ export default function TunnelConfig({
strokeLinejoin="round" strokeLinejoin="round"
className="text-blue-400 mt-0.5 flex-shrink-0" className="text-blue-400 mt-0.5 flex-shrink-0"
> >
<circle cx="12" cy="12" r="10" /> <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" /> <circle cx="9" cy="7" r="4" />
<path d="M2 12h20" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg> </svg>
<div className="flex-1"> <div className="flex-1">
<h4 className="text-blue-400 font-medium mb-2">Custom Domain Requires Sign In</h4> <h4 className="text-blue-400 font-medium mb-2">TCP Forwarding Requires Sign In</h4>
<p className="text-blue-300 text-sm mb-3"> <p className="text-blue-300 text-sm mb-3">
Custom domain support allows you to use your own domain name for professional tunneling. This To prevent abuse and ensure service quality, TCP forwarding requires user authentication. This
feature requires authentication to verify domain ownership and manage DNS settings. helps us maintain a reliable service for everyone.
</p> </p>
<p className="text-blue-300 text-sm mb-4"> <p className="text-blue-300 text-sm mb-4">
With a custom domain, you can create branded tunnel URLs like tunnel.yourdomain.com instead of TCP forwarding allows you to tunnel any TCP-based service like databases, game servers, SSH, and
using our default subdomains. custom applications.
</p> </p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
@@ -1026,7 +983,7 @@ export default function TunnelConfig({
</Link> </Link>
<button <button
onClick={() => { onClick={() => {
setShowCustomDomainLoginPrompt(false) setShowTcpLoginPrompt(false)
updateConfig({ type: "http", serverPort: 443 }) updateConfig({ type: "http", serverPort: 443 })
}} }}
className="text-blue-300 hover:text-blue-200 text-sm underline" className="text-blue-300 hover:text-blue-200 text-sm underline"
@@ -1040,7 +997,7 @@ export default function TunnelConfig({
)} )}
</div> </div>
{!showCustomDomainLoginPrompt && ( {!showTcpLoginPrompt && (
<div className="grid gap-4 md:grid-cols-2 mb-6"> <div className="grid gap-4 md:grid-cols-2 mb-6">
<div> <div>
<label className="block text-sm font-medium mb-2">Server Port (Internet Access)</label> <label className="block text-sm font-medium mb-2">Server Port (Internet Access)</label>
@@ -1059,7 +1016,8 @@ export default function TunnelConfig({
type="number" type="number"
value={localConfig.serverPort === 0 ? "" : localConfig.serverPort} value={localConfig.serverPort === 0 ? "" : localConfig.serverPort}
onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) || 0 })} onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) || 0 })}
className={`w-full bg-gray-800 border rounded-lg px-3 py-2 text-white font-mono focus:outline-none ${portError ? "border-red-500 focus:border-red-400" : "border-gray-700 focus:border-emerald-500" className={`w-full bg-gray-800 border rounded-lg px-3 py-2 text-white font-mono focus:outline-none ${
portError ? "border-red-500 focus:border-red-400" : "border-gray-700 focus:border-emerald-500"
}`} }`}
placeholder="0 for auto-assign" placeholder="0 for auto-assign"
min="0" min="0"
@@ -1096,7 +1054,7 @@ export default function TunnelConfig({
</div> </div>
)} )}
{!showCustomDomainLoginPrompt && selectedServer && ( {!showTcpLoginPrompt && selectedServer && (
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium mb-2">SSH Command</label> <label className="block text-sm font-medium mb-2">SSH Command</label>
<div className="relative"> <div className="relative">
@@ -1145,7 +1103,7 @@ export default function TunnelConfig({
</div> </div>
)} )}
{!showCustomDomainLoginPrompt && ( {!showTcpLoginPrompt && (
<div className="p-3 bg-gray-800 rounded-lg border border-gray-700"> <div className="p-3 bg-gray-800 rounded-lg border border-gray-700">
<p className="text-sm text-gray-300"> <p className="text-sm text-gray-300">
<span className="font-medium">Traffic Flow:</span> Internet {" "} <span className="font-medium">Traffic Flow:</span> Internet {" "}

View File

@@ -1,521 +0,0 @@
"use client"
export default function TunnlLogo() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={20} height={18}>
<path
d="M8.52 4c.296.02.574.086.863.164.09.027.18.05.27.074.066.02.066.02.132.035.29.079.578.149.871.211.332.075.66.153.989.235.492.125.984.246 1.48.355.344.078.684.16 1.027.246.11.028.215.051.325.078.402.098.8.196 1.203.297l.953.235c.023.008.05.011.074.02.16.038.32.073.48.109 1.305.285 1.305.285 1.688.52.031.015.059.034.09.054.262.168.422.37.586.637l.047.074c.238.414.187 1.015.187 1.48v1.649c0 .609-.023 1.144-.445 1.613-.39.367-.871.52-1.367.684-.059.02-.118.039-.18.062-.086.027-.168.059-.254.086-.176.059-.348.121-.523.184a8.12 8.12 0 0 1-.52.175c-.164.051-.324.114-.484.172-.102.04-.2.07-.301.102-.262.078-.52.168-.774.261-.652.231-1.308.458-1.964.68-.2.067-.399.133-.598.203a249.724 249.724 0 0 1-1.176.403c-.136.047-.273.093-.41.136-.05.02-.101.036-.156.055-.067.024-.137.047-.207.07-.04.012-.082.028-.121.04-.098.023-.098.023-.211-.016V12.69c.691-.175.691-.175.937-.23.028-.004.055-.012.086-.016.086-.02.176-.039.266-.058l.191-.04c.387-.085.774-.163 1.16-.238.356-.066.707-.144 1.059-.222.352-.078.703-.153 1.059-.215.355-.067.71-.137 1.062-.215.043-.008.086-.02.133-.027.515-.098.515-.098.918-.414.172-.278.152-.59.152-.91V9.98c.004-.09.004-.175.004-.261v-.657c.004-.039.004-.078.004-.117 0-.308-.05-.597-.203-.867-.067-.047-.067-.047-.149-.078l-.082-.04a.575.575 0 0 0-.086-.038l-.082-.04a1.447 1.447 0 0 0-.46-.093c-.036-.004-.07-.004-.106-.008-.039-.004-.074-.008-.113-.008a17.593 17.593 0 0 1-.766-.082c-.336-.043-.672-.062-1.012-.086-.261-.015-.52-.039-.777-.066-.344-.039-.687-.062-1.035-.082-.348-.023-.7-.05-1.047-.086-.332-.031-.668-.047-1-.062a4.771 4.771 0 0 1-.187-.61c-.207-.879-.653-1.832-1.356-2.41-.11-.098-.11-.098-.144-.207V4Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#f4f0ed",
fillOpacity: 1,
}}
/>
<path
d="M7.129 3.652c.035.016.035.016.07.035 1.043.481 1.867 1.415 2.285 2.5.27.743.27.743.196 1.083-.028-.004-.055-.004-.086-.008a49.636 49.636 0 0 0-1.864-.11c-.011-.02-.02-.043-.03-.066-.34-.82-.34-.82-.985-1.395v-.074l-.098-.035a3.2 3.2 0 0 1-.336-.14c-.62-.266-1.363-.243-1.988-.008-.79.332-1.242.941-1.586 1.722-.148.442-.129.934-.129 1.399 0 .066 0 .136-.004.207v.558c0 .2 0 .395-.004.59 0 .371-.004.738-.004 1.11 0 .421-.003.843-.003 1.265l-.012 2.598-.446.094c-.062.011-.062.011-.128.027-.86.176-.86.176-1.227.226-.09-.136-.086-.238-.086-.398V12.059c.004-.391 0-.778 0-1.164V8.53c0-.582.012-1.187.168-1.75.008-.031.016-.058.023-.09a4.958 4.958 0 0 1 1.36-2.23h.074l.04-.113h.108c.012-.024.02-.047.028-.07.058-.106.117-.137.219-.196.144-.086.144-.086.289-.176 1.195-.808 2.879-.836 4.156-.254Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#f2efec",
fillOpacity: 1,
}}
/>
<path
d="M12.488 7.957h.14c.13 0 .259.012.392.023a51.951 51.951 0 0 0 .996.07l.53.036.884.059c.093.007.191.011.289.02.133.007.27.019.406.026.043.004.082.008.125.008.059.004.059.004.113.012.051 0 .051 0 .102.004.082.015.082.015.195.094v.074l.113.039c.047.101.043.176.043.289 0 .043.004.086.004.133 0 .203.004.402.004.605 0 .106 0 .211.004.317 0 .156 0 .308.004.46v.145c0 .328 0 .328-.105.484a1.149 1.149 0 0 1-.504.192l-.075.012-.234.035c-.055.008-.113.015-.168.027-.36.055-.723.11-1.082.16-.176.028-.355.051-.531.078-.028.004-.059.012-.086.016-.18.027-.36.055-.54.086-.105.02-.214.035-.323.055-.055.007-.106.02-.16.027-.079.012-.153.027-.231.039-.063.012-.063.012-.133.023-.25.02-.422-.02-.613-.183-.149-.2-.156-.395-.156-.637v-.156c0-.055-.004-.11-.004-.164V8.59c.011-.203.047-.352.175-.508.149-.129.231-.125.426-.125ZM14.523 9.5a.528.528 0 0 0 0 .379c.07.094.07.094.204.125.132.008.132.008.222-.063a.594.594 0 0 0 .098-.363c-.05-.117-.05-.117-.149-.195-.164-.028-.27-.012-.375.117Zm1.114-.078c-.09.11-.078.203-.078.344.007.09.007.09.09.156.128.02.128.02.261 0 .098-.082.11-.137.13-.27-.009-.117-.009-.117-.083-.207-.113-.082-.191-.078-.32-.023Zm-2.317.156c-.043.125-.062.211-.008.34.051.074.051.074.16.16a.518.518 0 0 0 .352-.101c.086-.133.09-.247.059-.399-.055-.101-.055-.101-.149-.156-.187-.031-.289.012-.414.156Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#5aa680",
fillOpacity: 1,
}}
/>
<path
d="M9.68 12.77v2.808c-.293.129-.293.129-.414.172-.024.008-.051.016-.079.027-.027.008-.054.02-.082.028l-.09.03a3.392 3.392 0 0 1-.183.063c-.062.024-.125.047-.187.067-.09.031-.18.066-.274.098-.027.007-.055.019-.082.027a.613.613 0 0 1-.41.027v-2.965a7.29 7.29 0 0 1 .695-.183c.028-.008.059-.012.086-.02l.18-.035c.09-.02.18-.035.27-.055l.175-.035c.024-.004.05-.011.078-.015.11-.024.207-.04.317-.04Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#f4f0ee",
fillOpacity: 1,
}}
/>
<path
d="M6.703 9.59h3.133c.164.004.234.008.371.117.074.102.074.102.098.219-.024.113-.024.113-.098.195-.16.098-.289.094-.473.09H7.082c-.113 0-.23-.004-.344-.004h-.101c-.176 0-.305-.008-.446-.129-.043-.133-.043-.133-.039-.27.157-.222.305-.222.551-.218Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#f7f5f2",
fillOpacity: 1,
}}
/>
<path
d="M8.145 10.914c.027 0 .058 0 .09-.004h2.097c.094-.004.188 0 .281 0 .043 0 .043 0 .086-.004a.755.755 0 0 1 .446.133c.074.113.074.113.07.25-.031.133-.031.133-.09.2-.133.07-.258.066-.402.066h-.094c-.106 0-.207 0-.313-.004H8.031c-.152-.004-.281-.012-.414-.09-.054-.125-.054-.125-.074-.27.043-.12.078-.171.191-.234.137-.043.266-.047.41-.043Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#edeae7",
fillOpacity: 1,
}}
/>
<path
d="M8.555 8.324H9.949c.13-.004.254-.004.38-.004h.577c.16.032.215.09.313.22.004.12.004.12-.035.23-.09.093-.145.113-.27.128h-.144c-.04.004-.04.004-.079.004H9.02c-.06 0-.118 0-.176.004-.086 0-.168 0-.25-.004h-.145c-.14-.02-.183-.054-.27-.172-.015-.125-.015-.199.044-.312.105-.105.187-.09.332-.094Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#eeebe9",
fillOpacity: 1,
}}
/>
<path
d="M10.094 9.77c.062.011.125.023.187.039a.757.757 0 0 1 0 .23.431.431 0 0 1-.265.168c-.094.004-.188.004-.282.004H9.02c-.176 0-.348 0-.52-.004H7.074c-.117-.004-.23-.004-.344-.004H6.45c-.07-.012-.07-.012-.183-.086l3.828-.039v-.117h-.074v-.078h.074V9.77Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#9d9c9b",
fillOpacity: 1,
}}
/>
<path
d="M8.52 4c.335.016.648.105.972.191v.04h-.336c-.004.042-.011.085-.015.128-.024.141-.024.141-.098.22.05.023.098.05.148.073-.023.04-.046.078-.074.118-.031-.036-.058-.07-.094-.106-.039-.043-.082-.09-.12-.137l-.063-.066c-.02-.024-.04-.043-.059-.066-.02-.024-.039-.043-.058-.067a2.522 2.522 0 0 0-.145-.144C8.52 4.117 8.52 4.117 8.52 4Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#d2cfce",
fillOpacity: 1,
}}
/>
<path
d="M6.34 9.617c.488-.004.976-.008 1.46-.008.227 0 .454 0 .68-.004h.653c.082 0 .168 0 .25-.003H9.836c.234 0 .234 0 .355.074.028.027.028.027.051.054l-.035.079c-.05-.012-.102-.028-.152-.04v-.078c-1.223-.011-2.45-.023-3.715-.039v-.035Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#d9d5d6",
fillOpacity: 1,
}}
/>
<path
d="m13.707 11.324.102.012.074.012c-.2.117-.434.129-.66.164-.082.015-.164.027-.246.043l-.16.023-.145.024c-.14.015-.262 0-.402-.024l-.036-.117c.07.004.07.004.145.004.195 0 .383-.027.574-.059.035-.004.067-.011.102-.015.125-.02.246-.04.37-.07a.97.97 0 0 1 .282.003Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#589275",
fillOpacity: 1,
}}
/>
<path
d="M8.145 10.914h2.558c.152 0 .27.016.402.086-.14.047-.273.043-.421.043H9.473c-.196-.004-.391-.004-.586-.004H7.73c-.011.04-.027.074-.039.113a1.343 1.343 0 0 0-.074-.035c.04-.117.04-.117.113-.164.141-.039.27-.039.415-.039Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#c5c4c3",
fillOpacity: 1,
}}
/>
<path
d="M11.145 8.46c.035.04.035.04.074.08-.004.124-.012.202-.098.292-.121.07-.215.066-.351.066H9.387c-.125 0-.25 0-.375-.003H8.44c-.109-.012-.109-.012-.187-.086h2.742c.004-.082.004-.082-.039-.157h.148c.016-.062.028-.125.04-.191Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#bbbab9",
fillOpacity: 1,
}}
/>
<path
d="M7.953 13.117c.027.012.05.024.078.035-.039.016-.074.028-.113.04l.035 2.886c.102-.015.2-.027.3-.039-.1.106-.19.102-.335.113-.063-.062-.043-.144-.043-.23v-.657c0-.175 0-.35.004-.527v-1.585c.027-.012.05-.024.074-.036Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#999896",
fillOpacity: 1,
}}
/>
<path
d="M19.7 7.809h.038V9.96H19.7L19.664 8.5h-.039v-.23c.012-.012.023-.028.04-.04.007-.07.015-.14.019-.214.003-.04.007-.079.007-.118.004-.03.008-.058.008-.09Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#ebe6e6",
fillOpacity: 1,
}}
/>
<path
d="M13.207 8c.422.023.844.05 1.277.078-.011.035-.023.074-.039.113h-.71c-.012-.039-.028-.074-.04-.113h-.488V8Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#5d8d79",
fillOpacity: 1,
}}
/>
<path
d="M12.469 7.957h.12a.753.753 0 0 0 .095.004c.011.027.023.05.035.078-.114.04-.192.04-.309.043-.125.008-.195.016-.293.098a2.451 2.451 0 0 0-.148.203 4.044 4.044 0 0 1-.035-.113c.14-.262.25-.32.535-.313Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#5b957c",
fillOpacity: 1,
}}
/>
<path
d="M15.57 8.191h.309c.031 0 .059-.004.09-.004.148 0 .285.004.43.043v.079c-.278.004-.551.004-.829-.04v-.078Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#66a686",
fillOpacity: 1,
}}
/>
<path
d="M12.234 11.46h.75v.08c-.097.01-.195.026-.293.038l-.086.012a.654.654 0 0 1-.335-.012l-.036-.117Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#5e9b78",
fillOpacity: 1,
}}
/>
<path
d="M.715 14.23H.75c.04.31.043.614.04.922-.028-.011-.052-.023-.075-.035l-.024-.402c0-.035-.003-.074-.007-.113 0-.055 0-.055-.004-.11 0-.035-.004-.066-.004-.101 0-.082 0-.082.039-.16Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#d1d0ce",
fillOpacity: 1,
}}
/>
<path
d="M10.094 9.77c.062.011.125.023.187.039v.23l-.148.078a1.826 1.826 0 0 1-.04-.156h-.073v-.078h.074V9.77Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#e1dfde",
fillOpacity: 1,
}}
/>
<path
d="M9.457 9.617c.102-.004.207-.008.309-.008.027-.004.058-.004.09-.004.218-.003.218-.003.332.067.019.02.039.039.054.058l-.035.079c-.05-.012-.102-.028-.152-.04v-.078c-.024 0-.047 0-.075.004a2.35 2.35 0 0 1-.523-.043v-.035Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#cccbcb",
fillOpacity: 1,
}}
/>
<path
d="m16.398 8.23.223.04c.016.039.027.074.04.113.038.015.073.027.112.039l.036.23c-.11-.035-.11-.035-.164-.129-.07-.117-.122-.14-.247-.175V8.23Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#598871",
fillOpacity: 1,
}}
/>
<path
d="M15.309 11.04c.136.01.273.023.414.038v.04a3.446 3.446 0 0 1-.825.073c.09-.09.125-.09.247-.101a.758.758 0 0 0 .093-.008c.024 0 .047-.004.07-.004v-.039Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#528068",
fillOpacity: 1,
}}
/>
<path
d="m14.488 11.21.106.013c.023 0 .05.004.078.007v.04c-.266.05-.516.085-.79.078v-.04c.024-.007.048-.011.071-.015a.8.8 0 0 1 .094-.02l.094-.023c.222-.055.222-.055.347-.04Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#54856a",
fillOpacity: 1,
}}
/>
<path
d="M13.883 8.078h.601c-.011.035-.023.074-.039.113l-.218-.011-.122-.012c-.109-.016-.109-.016-.222-.09Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#59997b",
fillOpacity: 1,
}}
/>
<path
d="M13.734 5.27c.23-.016.418.039.637.113-.113.039-.144.043-.262 0v.078h-.113v-.078h-.074v-.074h-.188v-.04Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#b4b1b1",
fillOpacity: 1,
}}
/>
<path
d="M7.879 15.617h.039l.035.461c.102-.015.2-.027.3-.039-.1.106-.19.102-.335.113-.04-.035-.04-.035-.043-.148 0-.047.004-.094.004-.14v-.247Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#8f8e8d",
fillOpacity: 1,
}}
/>
<path
d="M17.074 10.809a.325.325 0 0 1 .035.152.746.746 0 0 1-.261.309c-.035-.012-.075-.028-.114-.04l.086-.058c.121-.113.176-.215.254-.363Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#a5a4a2",
fillOpacity: 1,
}}
/>
<path
d="M19.7 7.809h.038v.652c-.039.012-.074.027-.113.039v-.23c.012-.012.023-.028.04-.04.007-.07.015-.14.019-.214.003-.04.007-.079.007-.118.004-.03.008-.058.008-.09Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#d9d6d5",
fillOpacity: 1,
}}
/>
<path
d="M7.656 3.96c.219.106.219.106.262.192h-.113c-.012.051-.024.102-.04.157L7.73 4.19a.445.445 0 0 1-.074-.039v-.191Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#cac8c6",
fillOpacity: 1,
}}
/>
<path
d="M8.707 15.883a.647.647 0 0 1-.289.12.8.8 0 0 1-.094.02c-.023.004-.047.012-.07.016.082-.25.242-.187.453-.156Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#bbb4b2",
fillOpacity: 1,
}}
/>
<path
d="M10.094 15.23c.074.028.148.051.226.079.024-.028.047-.051.075-.079.05.028.097.051.148.079-.05.02-.105.039-.16.058-.047.016-.047.016-.09.035-.086.02-.086.02-.2-.02v-.152Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#b3b1b0",
fillOpacity: 1,
}}
/>
<path
d="M16.059 11.46c-.02.013-.043.02-.067.032-.082.04-.082.04-.133.11-.062.05-.062.05-.148.046-.027-.007-.059-.015-.09-.027l-.09-.023c-.035-.012-.035-.012-.07-.02v-.039c.2-.047.39-.09.598-.078Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#acaba9",
fillOpacity: 1,
}}
/>
<path
d="M6.266 9.652h.074c.015.051.027.102.039.157-.074.039-.074.039-.149.074l.036.234c-.082-.07-.11-.101-.125-.21a.395.395 0 0 1 .125-.255Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#a8a7a8",
fillOpacity: 1,
}}
/>
<path
d="M7.094 5.809c.035.011.074.023.11.039v.074c.026.016.05.027.077.039a.728.728 0 0 0-.039.078c.016.082.016.082.04.152a1.678 1.678 0 0 1-.266-.308c.027-.024.05-.051.078-.074Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#c2c0bb",
fillOpacity: 1,
}}
/>
<path
d="M11.145 8.46c.023.028.05.052.074.08-.016.21-.016.21-.114.308-.035-.016-.074-.028-.109-.04V8.73a.486.486 0 0 1-.039-.078h.148c.016-.062.028-.125.04-.191Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#c5c4c2",
fillOpacity: 1,
}}
/>
<path
d="M16.246 7.77h.563c-.036.011-.075.023-.11.039v.113l-.453-.113v-.04Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#b8b7b5",
fillOpacity: 1,
}}
/>
<path
d="M2.516 4.191c.023.012.046.028.074.04-.027.062-.05.128-.074.19h-.114c-.011.028-.027.052-.039.079-.023-.012-.05-.023-.074-.04.012-.038.023-.073.04-.112h.108c.028-.051.051-.102.079-.157Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#a8a8a6",
fillOpacity: 1,
}}
/>
<path
d="m3.64 3.578.075.04c-.024.01-.047.023-.074.034a.717.717 0 0 1-.094.051.717.717 0 0 1-.094.05c-.086.052-.086.052-.113.169-.012-.035-.024-.074-.04-.113-.023-.012-.046-.028-.073-.04.066-.035.136-.066.203-.097.035-.02.074-.04.113-.055a.727.727 0 0 1 .098-.039Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#a3a2a2",
fillOpacity: 1,
}}
/>
<path
d="M1.238 15c.125.012.246.023.375.04v.038c-.199.024-.394.05-.601.074.113-.074.113-.074.226-.074V15Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#999998",
fillOpacity: 1,
}}
/>
<path
d="M3.527 5.73h.075c-.012.125-.04.157-.133.243-.031.02-.059.043-.09.066v-.156h.074c.024-.051.047-.102.074-.153Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#bbbab9",
fillOpacity: 1,
}}
/>
<path
d="m6.004 3.348.18.011.097.008c.098.016.098.016.246.094h-.523v-.113Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#b0afac",
fillOpacity: 1,
}}
/>
<path
d="M9.367 15.613h.094c.035 0 .035 0 .07.004-.129.086-.222.098-.375.113-.011-.023-.023-.05-.039-.078.09-.043.149-.039.25-.039Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#a9a6a4",
fillOpacity: 1,
}}
/>
<path
d="M15.984 9.77c.012.023.024.05.04.078-.036.082-.036.082-.075.152h-.226c-.012-.023-.024-.05-.04-.078l.11-.012c.129-.012.129-.012.191-.14Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#4e7d64",
fillOpacity: 1,
}}
/>
<path
d="M12.082 8.04c.023.01.05.023.074.038-.14.262-.14.262-.222.305a.595.595 0 0 1 .148-.344Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#51836d",
fillOpacity: 1,
}}
/>
<path
d="M12.047 14.578c.168-.015.168-.015.254.055.015.02.027.039.043.058-.086.028-.172.051-.262.079.012-.051.023-.102.04-.153l-.075-.039Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#aea9a6",
fillOpacity: 1,
}}
/>
<path
d="M17.86 12.77c-.13.09-.223.097-.376.113.028-.012.051-.024.078-.035v-.078c.106-.055.184-.024.297 0Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#b4b1b0",
fillOpacity: 1,
}}
/>
<path
d="M15.496 9.46h.074l-.035.349h-.074c-.031-.114-.047-.172-.004-.282.012-.023.027-.043.04-.066Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#74b698",
fillOpacity: 1,
}}
/>
<path
d="m16.059 9.422-.075.117c-.015-.02-.03-.035-.046-.055a.72.72 0 0 0-.215-.136c.148-.063.21-.012.336.074Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#53866a",
fillOpacity: 1,
}}
/>
<path
d="M16.809 8c.05.012.101.023.152.04v.077h.113l.035.153c-.035.02-.035.02-.074.039-.113-.145-.113-.145-.113-.23-.035-.017-.074-.028-.113-.04V8Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#afadab",
fillOpacity: 1,
}}
/>
<path
d="M7.953 10.96c-.035.013-.07.02-.11.032a3.353 3.353 0 0 1-.113.047c-.011.04-.027.074-.039.113a1.343 1.343 0 0 0-.074-.035c.04-.117.04-.117.125-.164.102-.031.102-.031.211.008Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#979696",
fillOpacity: 1,
}}
/>
<path
d="M9.906 6.152c.149.078.149.078.227.157l-.078.039c-.024.05-.047.101-.075.152-.023-.113-.046-.227-.074-.348Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#b7b5b5",
fillOpacity: 1,
}}
/>
<path
d="M10.133 4.383c.152.02.277.035.41.117-.121.027-.121.027-.262.04-.09-.075-.09-.075-.148-.157Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#bab8b7",
fillOpacity: 1,
}}
/>
<path
d="M8.594 4.04c.035.01.074.022.113.038.023.078.023.078.035.152-.113 0-.113 0-.187-.039.011-.05.027-.101.039-.152Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#d3d1d1",
fillOpacity: 1,
}}
/>
<path
d="M15.309 11.04c.136.01.273.023.414.038v.04c-.137.01-.274.023-.414.034v-.113Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#3f7154",
fillOpacity: 1,
}}
/>
<path
d="M13.695 9.383v.039c-.039.012-.082.023-.125.035-.136.035-.136.035-.25.121v-.156c.13-.04.242-.04.375-.04Zm0 0"
style={{
stroke: "none",
fillRule: "nonzero",
fill: "#3e6854",
fillOpacity: 1,
}}
/>
</svg>
)
}

View File

@@ -1,7 +1,7 @@
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
export default defineConfig({ export default defineConfig({
dialect: "postgresql", dialect: "postgresql",
schema: "./lib/schema/*", schema: "./app/db/schema/*",
out: "./drizzle", out: "./drizzle",
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL! url: process.env.DATABASE_URL!

View File

@@ -1,50 +0,0 @@
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -1,326 +0,0 @@
{
"id": "be4362e0-2820-4d0f-b7f2-281416bccdd2",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,13 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1767088616990,
"tag": "0000_volatile_guardian",
"breakpoints": true
}
]
}

View File

@@ -1,8 +1,5 @@
import { createAuthClient } from "better-auth/react" import { createAuthClient } from "better-auth/react"
import { jwtClient } from "better-auth/client/plugins"
import { inferAdditionalFields } from "better-auth/client/plugins";
import { auth } from "@/lib/auth"
export const authClient = createAuthClient({ export const authClient = createAuthClient({
plugins: [jwtClient(), inferAdditionalFields<typeof auth>()], /** The base URL of the server (optional if you're using the same domain) */
baseURL: "http://localhost:3000"
}) })

View File

@@ -2,25 +2,14 @@ import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import * as schema from "@/lib/schema/auth" import * as schema from "@/lib/schema/auth"
import { jwt } from "better-auth/plugins";
export const auth = betterAuth({ export const auth = betterAuth({
plugins: [jwt()],
socialProviders: { socialProviders: {
google: { google: {
clientId: process.env.GOOGLE_CLIENT_ID!, clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}, },
}, },
user: {
additionalFields: {
sshIdentifier: {
type: "string",
nullable: false,
input: false,
}
},
},
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
schema: schema schema: schema

View File

@@ -1,15 +1,8 @@
import { relations, sql } from "drizzle-orm"; import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
export const user = pgTable("user", { export const user = pgTable("user", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
name: text("name").notNull(), name: text("name").notNull(),
sshIdentifier: text("ssh_identifier")
.notNull()
.unique()
.default(
sql`substr(encode(gen_random_bytes(16), 'hex'), 1, 32)`
),
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(), emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"), image: text("image"),
@@ -20,9 +13,7 @@ export const user = pgTable("user", {
.notNull(), .notNull(),
}); });
export const session = pgTable( export const session = pgTable("session", {
"session",
{
id: text("id").primaryKey(), id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(), token: text("token").notNull().unique(),
@@ -35,13 +26,9 @@ export const session = pgTable(
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
}, });
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = pgTable( export const account = pgTable("account", {
"account",
{
id: text("id").primaryKey(), id: text("id").primaryKey(),
accountId: text("account_id").notNull(), accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(), providerId: text("provider_id").notNull(),
@@ -59,13 +46,9 @@ export const account = pgTable(
updatedAt: timestamp("updated_at") updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, });
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = pgTable( export const verification = pgTable("verification", {
"verification",
{
id: text("id").primaryKey(), id: text("id").primaryKey(),
identifier: text("identifier").notNull(), identifier: text("identifier").notNull(),
value: text("value").notNull(), value: text("value").notNull(),
@@ -75,33 +58,4 @@ export const verification = pgTable(
.defaultNow() .defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const jwks = pgTable("jwks", {
id: text("id").primaryKey(),
publicKey: text("public_key").notNull(),
privateKey: text("private_key").notNull(),
createdAt: timestamp("created_at").notNull(),
expiresAt: timestamp("expires_at"),
}); });
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));

View File

@@ -2,7 +2,6 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
output: "standalone",
}; };
export default nextConfig; export default nextConfig;

228
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"better-auth": "^1.3.23", "better-auth": "^1.3.23",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"drizzle-orm": "^0.45.0", "drizzle-orm": "^0.44.5",
"net": "^1.0.2", "net": "^1.0.2",
"next": "^16.0.0", "next": "^16.0.0",
"pg": "^8.16.3", "pg": "^8.16.3",
@@ -21,7 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^24.0.0", "@types/node": "^20",
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@@ -43,6 +43,36 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@better-auth/core": {
"version": "1.4.9",
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.9.tgz",
"integrity": "sha512-JT2q4NDkQzN22KclUEoZ7qU6tl9HUTfK1ctg2oWlT87SEagkwJcnrUwS9VznL+u9ziOIfY27P0f7/jSnmvLcoQ==",
"peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"zod": "^4.1.12"
},
"peerDependencies": {
"@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "1.1.21",
"better-call": "1.1.7",
"jose": "^6.1.0",
"kysely": "^0.28.5",
"nanostores": "^1.0.1"
}
},
"node_modules/@better-auth/telemetry": {
"version": "1.4.9",
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.4.9.tgz",
"integrity": "sha512-Tthy1/Gmx+pYlbvRQPBTKfVei8+pJwvH1NZp+5SbhwA6K2EXIaoonx/K6N/AXYs2aKUpyR4/gzqDesDjL7zd6A==",
"dependencies": {
"@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "1.1.21"
},
"peerDependencies": {
"@better-auth/core": "1.4.9"
}
},
"node_modules/@better-auth/utils": { "node_modules/@better-auth/utils": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz",
@@ -1625,9 +1655,7 @@
} }
}, },
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1637,37 +1665,35 @@
"lightningcss": "1.30.2", "lightningcss": "1.30.2",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
"source-map-js": "^1.2.1", "source-map-js": "^1.2.1",
"tailwindcss": "4.1.18" "tailwindcss": "4.1.17"
} }
}, },
"node_modules/@tailwindcss/oxide": { "node_modules/@tailwindcss/oxide": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-android-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.17",
"@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.17",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.17",
"@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.17",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18" "@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
} }
}, },
"node_modules/@tailwindcss/oxide-android-arm64": { "node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1682,9 +1708,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-darwin-arm64": { "node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1699,9 +1725,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-darwin-x64": { "node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1716,9 +1742,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-freebsd-x64": { "node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1733,9 +1759,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1750,9 +1776,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": { "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1767,9 +1793,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm64-musl": { "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1784,9 +1810,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-x64-gnu": { "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1801,9 +1827,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-x64-musl": { "node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1818,9 +1844,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi": { "node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
"bundleDependencies": [ "bundleDependencies": [
"@napi-rs/wasm-runtime", "@napi-rs/wasm-runtime",
"@emnapi/core", "@emnapi/core",
@@ -1836,10 +1862,10 @@
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/core": "^1.7.1", "@emnapi/core": "^1.6.0",
"@emnapi/runtime": "^1.7.1", "@emnapi/runtime": "^1.6.0",
"@emnapi/wasi-threads": "^1.1.0", "@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7",
"@tybys/wasm-util": "^0.10.1", "@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
@@ -1848,7 +1874,7 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.7.1", "version": "1.6.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@@ -1859,7 +1885,7 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.7.1", "version": "1.6.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@@ -1879,14 +1905,14 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.0", "version": "1.0.7",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/core": "^1.7.1", "@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.7.1", "@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.10.1" "@tybys/wasm-util": "^0.10.1"
} }
}, },
@@ -1908,9 +1934,9 @@
"optional": true "optional": true
}, },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1925,9 +1951,7 @@
} }
}, },
"node_modules/@tailwindcss/oxide-win32-x64-msvc": { "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1942,17 +1966,15 @@
} }
}, },
"node_modules/@tailwindcss/postcss": { "node_modules/@tailwindcss/postcss": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.18", "@tailwindcss/node": "4.1.17",
"@tailwindcss/oxide": "4.1.18", "@tailwindcss/oxide": "4.1.17",
"postcss": "^8.4.41", "postcss": "^8.4.41",
"tailwindcss": "4.1.18" "tailwindcss": "4.1.17"
} }
}, },
"node_modules/@types/d3-color": { "node_modules/@types/d3-color": {
@@ -1996,13 +2018,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.7", "version": "20.19.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.7.tgz",
"integrity": "sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/pg": { "node_modules/@types/pg": {
@@ -2019,9 +2039,7 @@
} }
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.8", "version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
@@ -2058,13 +2076,13 @@
} }
}, },
"node_modules/better-auth": { "node_modules/better-auth": {
"version": "1.4.10", "version": "1.4.9",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.10.tgz", "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.9.tgz",
"integrity": "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg==", "integrity": "sha512-usSdjuyTzZwIvM8fjF8YGhPncxV3MAg3dHUO9uPUnf0yklXUSYISiH1+imk6/Z+UBqsscyyPRnbIyjyK97p7YA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@better-auth/core": "1.4.10", "@better-auth/core": "1.4.9",
"@better-auth/telemetry": "1.4.10", "@better-auth/telemetry": "1.4.9",
"@better-auth/utils": "0.3.0", "@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "1.1.21", "@better-fetch/fetch": "1.1.21",
"@noble/ciphers": "^2.0.0", "@noble/ciphers": "^2.0.0",
@@ -2153,36 +2171,6 @@
} }
} }
}, },
"node_modules/better-auth/node_modules/@better-auth/core": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.10.tgz",
"integrity": "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg==",
"peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"zod": "^4.1.12"
},
"peerDependencies": {
"@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "1.1.21",
"better-call": "1.1.7",
"jose": "^6.1.0",
"kysely": "^0.28.5",
"nanostores": "^1.0.1"
}
},
"node_modules/better-auth/node_modules/@better-auth/telemetry": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.4.10.tgz",
"integrity": "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ==",
"dependencies": {
"@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "1.1.21"
},
"peerDependencies": {
"@better-auth/core": "1.4.10"
}
},
"node_modules/better-call": { "node_modules/better-call": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.7.tgz", "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.7.tgz",
@@ -2412,9 +2400,9 @@
} }
}, },
"node_modules/drizzle-orm": { "node_modules/drizzle-orm": {
"version": "0.45.1", "version": "0.44.7",
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz",
"integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"peerDependencies": { "peerDependencies": {
@@ -3437,9 +3425,7 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.18", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
@@ -3995,9 +3981,7 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.16.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },

View File

@@ -27,7 +27,7 @@
"dependencies": { "dependencies": {
"better-auth": "^1.3.23", "better-auth": "^1.3.23",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"drizzle-orm": "^0.45.0", "drizzle-orm": "^0.44.5",
"net": "^1.0.2", "net": "^1.0.2",
"next": "^16.0.0", "next": "^16.0.0",
"pg": "^8.16.3", "pg": "^8.16.3",
@@ -38,7 +38,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^24.0.0", "@types/node": "^20",
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",

View File

@@ -2,8 +2,6 @@
"extends": [ "extends": [
"config:recommended" "config:recommended"
], ],
"prConcurrentLimit": 1,
"prHourlyLimit": 1,
"packageRules": [ "packageRules": [
{ {
"matchUpdateTypes": [ "matchUpdateTypes": [
@@ -12,10 +10,9 @@
"pin", "pin",
"digest" "digest"
], ],
"groupName": "all-dependencies",
"automerge": true, "automerge": true,
"matchPackageNames": [ "baseBranchPatterns": [
"*" "staging"
] ]
} }
] ]

View File

@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -15,7 +11,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "react-jsx", "jsx": "preserve",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -23,19 +19,9 @@
} }
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["./*"]
"./*"
]
} }
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"next-env.d.ts", "exclude": ["node_modules"]
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }