mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-07 21:29:06 +00:00
Initial commit
This commit is contained in:
61
routes/api/hawser/connect/+server.ts
Normal file
61
routes/api/hawser/connect/+server.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Hawser Edge WebSocket Connect Endpoint
|
||||
*
|
||||
* This endpoint handles WebSocket connections from Hawser agents running in Edge mode.
|
||||
* In development: WebSocket is handled by Bun.serve in vite.config.ts on port 5174
|
||||
* In production: WebSocket is handled by the server wrapper in server.ts
|
||||
*
|
||||
* The HTTP GET endpoint returns connection info for clients.
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { isEdgeConnected, getAllEdgeConnections } from '$lib/server/hawser';
|
||||
|
||||
/**
|
||||
* GET /api/hawser/connect
|
||||
* Returns status of the Hawser Edge connection endpoint
|
||||
* This is used for health checks and debugging
|
||||
*/
|
||||
export const GET: RequestHandler = async () => {
|
||||
const connections = getAllEdgeConnections();
|
||||
const connectionList = Array.from(connections.entries()).map(([envId, conn]) => ({
|
||||
environmentId: envId,
|
||||
agentId: conn.agentId,
|
||||
agentName: conn.agentName,
|
||||
agentVersion: conn.agentVersion,
|
||||
dockerVersion: conn.dockerVersion,
|
||||
hostname: conn.hostname,
|
||||
capabilities: conn.capabilities,
|
||||
connectedAt: conn.connectedAt.toISOString(),
|
||||
lastHeartbeat: conn.lastHeartbeat.toISOString()
|
||||
}));
|
||||
|
||||
return json({
|
||||
status: 'ready',
|
||||
message: 'Hawser Edge WebSocket endpoint. Connect via WebSocket.',
|
||||
protocol: 'wss://<host>/api/hawser/connect',
|
||||
activeConnections: connectionList.length,
|
||||
connections: connectionList
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/hawser/connect
|
||||
* This is a fallback for non-WebSocket clients.
|
||||
* Returns instructions for connecting via WebSocket.
|
||||
*/
|
||||
export const POST: RequestHandler = async () => {
|
||||
return json(
|
||||
{
|
||||
error: 'WebSocket required',
|
||||
message: 'This endpoint requires a WebSocket connection. Use the ws:// or wss:// protocol.',
|
||||
instructions: [
|
||||
'1. Generate a token in Settings > Environments > [Environment] > Hawser',
|
||||
'2. Configure your Hawser agent with DOCKHAND_SERVER_URL and TOKEN',
|
||||
'3. The agent will connect automatically'
|
||||
]
|
||||
},
|
||||
{ status: 426 }
|
||||
); // 426 Upgrade Required
|
||||
};
|
||||
122
routes/api/hawser/tokens/+server.ts
Normal file
122
routes/api/hawser/tokens/+server.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Hawser Token Management API
|
||||
*
|
||||
* Handles CRUD operations for Hawser agent tokens.
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { db, hawserTokens, eq, desc } from '$lib/server/db/drizzle';
|
||||
import { generateHawserToken, revokeHawserToken } from '$lib/server/hawser';
|
||||
|
||||
/**
|
||||
* GET /api/hawser/tokens
|
||||
* List all Hawser tokens (without revealing full token values)
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
if (auth.authEnabled && !auth.isAuthenticated) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (auth.authEnabled && !auth.isAdmin) {
|
||||
return json({ error: 'Admin access required' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await db
|
||||
.select({
|
||||
id: hawserTokens.id,
|
||||
tokenPrefix: hawserTokens.tokenPrefix,
|
||||
name: hawserTokens.name,
|
||||
environmentId: hawserTokens.environmentId,
|
||||
isActive: hawserTokens.isActive,
|
||||
lastUsed: hawserTokens.lastUsed,
|
||||
createdAt: hawserTokens.createdAt,
|
||||
expiresAt: hawserTokens.expiresAt
|
||||
})
|
||||
.from(hawserTokens)
|
||||
.orderBy(desc(hawserTokens.createdAt));
|
||||
|
||||
return json(tokens);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Hawser tokens:', error);
|
||||
return json({ error: 'Failed to fetch tokens' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/hawser/tokens
|
||||
* Generate a new Hawser token
|
||||
*
|
||||
* Body: { name: string, environmentId: number, expiresAt?: string }
|
||||
* Returns: { token: string, tokenId: number } - token is only shown ONCE
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
if (auth.authEnabled && !auth.isAuthenticated) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (auth.authEnabled && !auth.isAdmin) {
|
||||
return json({ error: 'Admin access required' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, environmentId, expiresAt, rawToken } = body;
|
||||
|
||||
if (!name || typeof name !== 'string') {
|
||||
return json({ error: 'Token name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!environmentId || typeof environmentId !== 'number') {
|
||||
return json({ error: 'Environment ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await generateHawserToken(name, environmentId, expiresAt, rawToken);
|
||||
|
||||
return json({
|
||||
token: result.token,
|
||||
tokenId: result.tokenId,
|
||||
message: 'Token generated successfully. Save this token - it will not be shown again.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating Hawser token:', error);
|
||||
return json({ error: 'Failed to generate token' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE /api/hawser/tokens
|
||||
* Delete (revoke) a token by ID
|
||||
*
|
||||
* Query: ?id=<token_id>
|
||||
*/
|
||||
export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
if (auth.authEnabled && !auth.isAuthenticated) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (auth.authEnabled && !auth.isAdmin) {
|
||||
return json({ error: 'Admin access required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const tokenId = url.searchParams.get('id');
|
||||
if (!tokenId) {
|
||||
return json({ error: 'Token ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await revokeHawserToken(parseInt(tokenId, 10));
|
||||
return json({ success: true, message: 'Token revoked' });
|
||||
} catch (error) {
|
||||
console.error('Error revoking Hawser token:', error);
|
||||
return json({ error: 'Failed to revoke token' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user