diff --git a/apps/web/src/pages/nodes/index.tsx b/apps/web/src/pages/nodes/index.tsx
index aee8047..12b7755 100644
--- a/apps/web/src/pages/nodes/index.tsx
+++ b/apps/web/src/pages/nodes/index.tsx
@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useParams, Link } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Plus, Network, Wifi, WifiOff } from 'lucide-react';
+import { Plus, Network, Wifi, WifiOff, Copy, Check } from 'lucide-react';
+import { toast } from 'sonner';
import { api } from '@/lib/api';
import { formatBytes } from '@/lib/utils';
import { Button } from '@/components/ui/button';
@@ -16,6 +17,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
+ DialogDescription,
} from '@/components/ui/dialog';
interface NodeItem {
@@ -29,6 +31,10 @@ interface NodeItem {
isOnline: boolean;
}
+interface CreatedNode extends NodeItem {
+ daemonToken: string;
+}
+
interface PaginatedResponse
{
data: T[];
meta: { total: number };
@@ -38,6 +44,9 @@ export function NodesPage() {
const { orgId } = useParams();
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
+ const [tokenDialog, setTokenDialog] = useState(false);
+ const [createdToken, setCreatedToken] = useState('');
+ const [copied, setCopied] = useState(false);
const [name, setName] = useState('');
const [fqdn, setFqdn] = useState('');
const [daemonPort, setDaemonPort] = useState(8443);
@@ -52,17 +61,28 @@ export function NodesPage() {
const createMutation = useMutation({
mutationFn: (body: Record) =>
- api.post(`/organizations/${orgId}/nodes`, body),
- onSuccess: () => {
+ api.post(`/organizations/${orgId}/nodes`, body),
+ onSuccess: (node) => {
queryClient.invalidateQueries({ queryKey: ['nodes', orgId] });
setOpen(false);
setName('');
setFqdn('');
+ // Show token dialog
+ setCreatedToken(node.daemonToken);
+ setTokenDialog(true);
+ setCopied(false);
},
});
const nodes = data?.data ?? [];
+ const handleCopyToken = async () => {
+ await navigator.clipboard.writeText(createdToken);
+ setCopied(true);
+ toast.success('Token copied to clipboard');
+ setTimeout(() => setCopied(false), 2000);
+ };
+
return (
@@ -151,6 +171,45 @@ export function NodesPage() {
+ {/* Token display dialog */}
+
+
{nodes.map((node) => (
diff --git a/apps/web/src/pages/server/config.tsx b/apps/web/src/pages/server/config.tsx
index 3b44548..3f2a51a 100644
--- a/apps/web/src/pages/server/config.tsx
+++ b/apps/web/src/pages/server/config.tsx
@@ -112,7 +112,7 @@ function ConfigEditor({
const saveMutation = useMutation({
mutationFn: (data: { entries: ConfigEntry[] }) =>
- api.patch(
+ api.put(
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
data,
),
diff --git a/apps/web/src/pages/servers/index.tsx b/apps/web/src/pages/servers/index.tsx
new file mode 100644
index 0000000..965210c
--- /dev/null
+++ b/apps/web/src/pages/servers/index.tsx
@@ -0,0 +1,90 @@
+import { useParams, Link } from 'react-router';
+import { useQuery } from '@tanstack/react-query';
+import { Server, Plus } from 'lucide-react';
+import { api } from '@/lib/api';
+import { Card, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { statusBadgeVariant } from '@/lib/utils';
+
+interface ServerSummary {
+ id: string;
+ uuid: string;
+ name: string;
+ status: string;
+ gameName: string;
+ nodeName: string;
+ port: number;
+}
+
+interface PaginatedResponse
{
+ data: T[];
+ meta: { total: number; page: number; perPage: number; totalPages: number };
+}
+
+export function ServersPage() {
+ const { orgId } = useParams();
+
+ const { data: serversData } = useQuery({
+ queryKey: ['servers', orgId],
+ queryFn: () => api.get>(`/organizations/${orgId}/servers`),
+ });
+
+ const servers = serversData?.data ?? [];
+
+ return (
+
+
+
+
Servers
+
+ {servers.length} server{servers.length !== 1 ? 's' : ''}
+
+
+
+
+
+
+
+ {servers.length === 0 ? (
+
+
+
+ No servers yet
+
+
+
+
+
+ ) : (
+
+ {servers.map((server) => (
+
+
+
+
+
+
+
+
+
{server.name}
+
+ {server.gameName} · {server.nodeName} · :{server.port}
+
+
+
+ {server.status}
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/pages/settings/members.tsx b/apps/web/src/pages/settings/members.tsx
index 9220d45..069f501 100644
--- a/apps/web/src/pages/settings/members.tsx
+++ b/apps/web/src/pages/settings/members.tsx
@@ -33,11 +33,13 @@ export function MembersPage() {
const [email, setEmail] = useState('');
const [role, setRole] = useState<'admin' | 'user'>('user');
- const { data: members } = useQuery({
+ const { data: membersData } = useQuery({
queryKey: ['members', orgId],
- queryFn: () => api.get(`/organizations/${orgId}/members`),
+ queryFn: () => api.get<{ data: Member[] }>(`/organizations/${orgId}/members`),
});
+ const members = membersData?.data ?? [];
+
const addMutation = useMutation({
mutationFn: (body: { email: string; role: string }) =>
api.post(`/organizations/${orgId}/members`, body),
@@ -114,7 +116,7 @@ export function MembersPage() {
- {(members ?? []).map((member) => (
+ {members.map((member) => (
{member.username}
diff --git a/apps/web/src/stores/auth.ts b/apps/web/src/stores/auth.ts
index c8eb87c..67a6c5d 100644
--- a/apps/web/src/stores/auth.ts
+++ b/apps/web/src/stores/auth.ts
@@ -56,8 +56,8 @@ export const useAuthStore = create
((set) => ({
fetchUser: async () => {
try {
- const user = await api.get('/auth/me');
- set({ user, isAuthenticated: true, isLoading: false });
+ const data = await api.get<{ user: User }>('/auth/me');
+ set({ user: data.user, isAuthenticated: true, isLoading: false });
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
set({ user: null, isAuthenticated: false, isLoading: false });