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()