fix: resolve frontend routing, API mismatches, and missing UI components
- Add servers list page and missing routes (servers, settings redirect, account security)
- Fix members page .map error (API returns { data } wrapper, not flat array)
- Fix auth store fetchUser expecting flat User but API returns { user } wrapper
- Add node token display dialog after creation
- Add allocation management UI to node detail page
- Add account security page with password change
- Add change-password API endpoint
- Add node servers and stats API endpoints
- Fix config save using PATCH instead of PUT, add api.put method
- Fix audit logs field name mismatch (userName vs username)
- Replace admin nodes page to avoid orgId dependency
- Remove duplicate sidebar nav items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,39 @@ export default async function authRoutes(app: FastifyInstance) {
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// POST /api/auth/change-password
|
||||
app.post('/change-password', { onRequest: [app.authenticate] }, async (request) => {
|
||||
const { currentPassword, newPassword } = request.body as {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
if (!currentPassword || !newPassword || newPassword.length < 8) {
|
||||
throw AppError.badRequest('New password must be at least 8 characters');
|
||||
}
|
||||
|
||||
const user = await app.db.query.users.findFirst({
|
||||
where: eq(users.id, request.user.sub),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw AppError.notFound('User not found');
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(user.passwordHash, currentPassword);
|
||||
if (!isValid) {
|
||||
throw AppError.unauthorized('Current password is incorrect', 'INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
const newHash = await hashPassword(newPassword);
|
||||
await app.db
|
||||
.update(users)
|
||||
.set({ passwordHash: newHash, updatedAt: new Date() })
|
||||
.where(eq(users.id, user.id));
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// GET /api/auth/me
|
||||
app.get('/me', { onRequest: [app.authenticate] }, async (request) => {
|
||||
const payload = request.user;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { nodes, allocations } from '@source/database';
|
||||
import { nodes, allocations, servers, games } from '@source/database';
|
||||
import { AppError } from '../../lib/errors.js';
|
||||
import { requirePermission } from '../../lib/permissions.js';
|
||||
import { createAuditLog } from '../../lib/audit.js';
|
||||
@@ -133,6 +133,58 @@ export default async function nodeRoutes(app: FastifyInstance) {
|
||||
return reply.code(204).send();
|
||||
});
|
||||
|
||||
// GET /api/organizations/:orgId/nodes/:nodeId/servers
|
||||
app.get('/:nodeId/servers', { schema: NodeParamSchema }, async (request) => {
|
||||
const { orgId, nodeId } = request.params as { orgId: string; nodeId: string };
|
||||
await requirePermission(request, orgId, 'node.read');
|
||||
|
||||
const serverList = await app.db
|
||||
.select({
|
||||
id: servers.id,
|
||||
name: servers.name,
|
||||
status: servers.status,
|
||||
memoryLimit: servers.memoryLimit,
|
||||
cpuLimit: servers.cpuLimit,
|
||||
gameName: games.name,
|
||||
})
|
||||
.from(servers)
|
||||
.leftJoin(games, eq(servers.gameId, games.id))
|
||||
.where(and(eq(servers.nodeId, nodeId), eq(servers.organizationId, orgId)));
|
||||
|
||||
return { data: serverList };
|
||||
});
|
||||
|
||||
// GET /api/organizations/:orgId/nodes/:nodeId/stats
|
||||
// Returns basic stats from DB; real-time stats come from daemon via gRPC
|
||||
app.get('/:nodeId/stats', { schema: NodeParamSchema }, async (request) => {
|
||||
const { orgId, nodeId } = request.params as { orgId: string; nodeId: string };
|
||||
await requirePermission(request, orgId, 'node.read');
|
||||
|
||||
const node = await app.db.query.nodes.findFirst({
|
||||
where: and(eq(nodes.id, nodeId), eq(nodes.organizationId, orgId)),
|
||||
});
|
||||
if (!node) throw AppError.notFound('Node not found');
|
||||
|
||||
const serverList = await app.db
|
||||
.select({ id: servers.id, status: servers.status })
|
||||
.from(servers)
|
||||
.where(eq(servers.nodeId, nodeId));
|
||||
|
||||
const totalServers = serverList.length;
|
||||
const activeServers = serverList.filter((s) => s.status === 'running').length;
|
||||
|
||||
return {
|
||||
cpuPercent: 0,
|
||||
memoryUsed: 0,
|
||||
memoryTotal: node.memoryTotal,
|
||||
diskUsed: 0,
|
||||
diskTotal: node.diskTotal,
|
||||
activeServers,
|
||||
totalServers,
|
||||
uptime: 0,
|
||||
};
|
||||
});
|
||||
|
||||
// === Allocations ===
|
||||
|
||||
// GET /api/organizations/:orgId/nodes/:nodeId/allocations
|
||||
|
||||
Reference in New Issue
Block a user