feat: implement relay server with lease management and a frontend admin interface.

sinwoojin committed Nov 27, 2025 at 15:56 UTC b3d73482d814a854a6631b73bb3fbbb7ee750350
3 files changed +33 -9
cmd/relay-server/frontend/src/hooks/useAdmin.ts
+18 -6
@@ -1,16 +1,28 @@
1 import { useCallback, useEffect, useState } from "react";
2
3 -export interface LeaseEntry {
4 - Lease: {
5 - Identity: { Id: string };
6 - Name: string;
7 - };
3 +export interface LeaseMetadata {
4 + description: string;
5 + tags: string[];
6 + thumbnail: string;
7 + owner: string;
8 + hide: boolean;
9 +}
10 +
11 +export interface LeaseEntryParsed {
12 + ConnectionID: number;
13 Expires: string;
14 LastSeen: string;
15 + Lease: {
16 + identity: { id: string; public_key: string };
17 + expires: number;
18 + name: string;
19 + alpn: string[];
20 + metadata: LeaseMetadata;
21 + };
22 }
23
24 export function useAdmin() {
13 - const [leases, setLeases] = useState<LeaseEntry[]>([]);
25 + const [leases, setLeases] = useState<LeaseEntryParsed[]>([]);
26 const [bannedLeases, setBannedLeases] = useState<string[]>([]);
27 const [loading, setLoading] = useState(true);
28 const [error, setError] = useState("");
cmd/relay-server/frontend/src/pages/Admin.tsx
+3 -3
@@ -16,12 +16,12 @@ export function Admin() {
16 <h2 className="text-2xl font-semibold mb-4">Active Leases</h2>
17 <div className="grid gap-4">
18 {leases.map((entry, i) => {
19 - const id = entry.Lease.Identity.Id;
19 + const id = entry.Lease.identity.id;
20 const isBanned = bannedLeases.includes(id);
21 return (
22 <div key={i} className="bg-card p-4 rounded-lg flex justify-between items-center border border-border">
23 <div>
24 - <p className="font-bold text-lg">{entry.Lease.Name || "(Unnamed)"}</p>
24 + <p className="font-bold text-lg">{entry.Lease.name || "(Unnamed)"}</p>
25 <p className="text-sm text-muted-foreground font-mono break-all">{id}</p>
26 <p className="text-xs text-muted-foreground">Expires: {new Date(entry.Expires).toLocaleString()}</p>
27 </div>
@@ -34,7 +34,7 @@ export function Admin() {
34 }`}
35 >
36 {isBanned ? "Unban" : "Ban"}
37 - </button>
37 + </button>
38 </div>
39 );
40 })}
portal/relay.go
+12
@@ -301,6 +301,18 @@ func (g *RelayServer) IsConnectionActive(connectionID int64) bool {
301 return exists
302 }
303
304 +// CloseConnection closes the connection with the given ID
305 +func (g *RelayServer) CloseConnection(connectionID int64) {
306 + g.connectionsLock.Lock()
307 + connection, exists := g.connections[connectionID]
308 + g.connectionsLock.Unlock()
309 +
310 + if exists {
311 + log.Info().Int64("conn_id", connectionID).Msg("[RelayServer] Force closing connection")
312 + connection.conn.Close()
313 + }
314 +}
315 +
316 // GetAllLeaseEntries returns all lease entries from the lease manager
317 func (g *RelayServer) GetAllLeaseEntries() []*LeaseEntry {
318 g.leaseManager.leasesLock.RLock()