Initial commit

This commit is contained in:
Jarek Krochmalski
2025-12-28 21:16:03 +01:00
commit 62e3c6439e
552 changed files with 104858 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getEnvironment, updateEnvironment, deleteEnvironment, getEnvironmentPublicIps, setEnvironmentPublicIp, deleteEnvironmentPublicIp, deleteEnvUpdateCheckSettings, getGitStacksForEnvironmentOnly, deleteGitStack } from '$lib/server/db';
import { clearDockerClientCache } from '$lib/server/docker';
import { deleteGitStackFiles } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager';
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
import { unregisterSchedule } from '$lib/server/scheduler';
import { closeEdgeConnection } from '$lib/server/hawser';
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
// Get public IP for this environment
const publicIps = await getEnvironmentPublicIps();
const publicIp = publicIps[id.toString()] || null;
// Parse labels from JSON string to array
return json({
...env,
labels: parseLabels(env.labels as string | null),
publicIp
});
} catch (error) {
console.error('Failed to get environment:', error);
return json({ error: 'Failed to get environment' }, { status: 500 });
}
};
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
const data = await request.json();
// Clear cached Docker client before updating
clearDockerClientCache(id);
// Handle labels - only update if provided in the request
const labels = data.labels !== undefined
? serializeLabels(Array.isArray(data.labels) ? data.labels.slice(0, MAX_LABELS) : [])
: undefined;
const env = await updateEnvironment(id, {
name: data.name,
host: data.host,
port: data.port,
protocol: data.protocol,
tlsCa: data.tlsCa,
tlsCert: data.tlsCert,
tlsKey: data.tlsKey,
icon: data.icon,
socketPath: data.socketPath,
collectActivity: data.collectActivity,
collectMetrics: data.collectMetrics,
highlightChanges: data.highlightChanges,
labels: labels,
connectionType: data.connectionType,
hawserToken: data.hawserToken
});
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
// Notify subprocesses if collectActivity or collectMetrics setting changed
if (data.collectActivity !== undefined || data.collectMetrics !== undefined) {
refreshSubprocessEnvironments();
}
// Handle public IP - update if provided in request
if (data.publicIp !== undefined) {
await setEnvironmentPublicIp(id, data.publicIp || null);
}
// Get current public IP for response
const publicIps = await getEnvironmentPublicIps();
const publicIp = publicIps[id.toString()] || null;
// Parse labels from JSON string to array
return json({
...env,
labels: parseLabels(env.labels as string | null),
publicIp
});
} catch (error) {
console.error('Failed to update environment:', error);
return json({ error: 'Failed to update environment' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'delete')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Close Edge connection if this is a Hawser Edge environment
// This rejects any pending requests and closes the WebSocket
closeEdgeConnection(id);
// Clear cached Docker client before deleting
clearDockerClientCache(id);
// Clean up git stacks for this environment
const gitStacks = await getGitStacksForEnvironmentOnly(id);
for (const stack of gitStacks) {
// Unregister schedule if auto-update was enabled
if (stack.autoUpdate) {
unregisterSchedule(stack.id, 'git_stack_sync');
}
// Delete git stack files from filesystem
deleteGitStackFiles(stack.id);
// Delete git stack from database
await deleteGitStack(stack.id);
}
const success = await deleteEnvironment(id);
if (!success) {
return json({ error: 'Cannot delete this environment' }, { status: 400 });
}
// Clean up public IP entry for this environment
await deleteEnvironmentPublicIp(id);
// Clean up update check settings and unregister schedule
await deleteEnvUpdateCheckSettings(id);
unregisterSchedule(id, 'env_update_check');
// Notify subprocesses to stop collecting from deleted environment
refreshSubprocessEnvironments();
return json({ success: true });
} catch (error) {
console.error('Failed to delete environment:', error);
return json({ error: 'Failed to delete environment' }, { status: 500 });
}
};

View File

@@ -0,0 +1,78 @@
import { json } from '@sveltejs/kit';
import {
getEnvironmentNotifications,
createEnvironmentNotification,
getEnvironment,
getNotificationSettings,
type NotificationEventType
} from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import type { RequestHandler } from './$types';
// GET /api/environments/[id]/notifications - List all notification configurations for an environment
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('notifications', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const envId = parseInt(params.id);
if (isNaN(envId)) {
return json({ error: 'Invalid environment ID' }, { status: 400 });
}
const env = await getEnvironment(envId);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
try {
const notifications = await getEnvironmentNotifications(envId);
return json(notifications);
} catch (error) {
console.error('Error fetching environment notifications:', error);
return json({ error: 'Failed to fetch environment notifications' }, { status: 500 });
}
};
// POST /api/environments/[id]/notifications - Add a notification channel to an environment
export const POST: RequestHandler = async ({ params, request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('notifications', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const envId = parseInt(params.id);
if (isNaN(envId)) {
return json({ error: 'Invalid environment ID' }, { status: 400 });
}
const env = await getEnvironment(envId);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
try {
const body = await request.json();
const { notificationId, enabled, eventTypes } = body;
if (!notificationId) {
return json({ error: 'notificationId is required' }, { status: 400 });
}
const notification = await createEnvironmentNotification({
environmentId: envId,
notificationId,
enabled: enabled !== false,
eventTypes: eventTypes as NotificationEventType[]
});
return json(notification);
} catch (error: any) {
console.error('Error creating environment notification:', error);
if (error.message?.includes('UNIQUE constraint failed')) {
return json({ error: 'This notification channel is already configured for this environment' }, { status: 409 });
}
return json({ error: error.message || 'Failed to create environment notification' }, { status: 500 });
}
};

View File

@@ -0,0 +1,103 @@
import { json } from '@sveltejs/kit';
import {
getEnvironmentNotification,
updateEnvironmentNotification,
deleteEnvironmentNotification,
getEnvironment,
type NotificationEventType
} from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import type { RequestHandler } from './$types';
// GET /api/environments/[id]/notifications/[notificationId] - Get a specific environment notification
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('notifications', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const envId = parseInt(params.id);
const notifId = parseInt(params.notificationId);
if (isNaN(envId) || isNaN(notifId)) {
return json({ error: 'Invalid ID' }, { status: 400 });
}
const env = await getEnvironment(envId);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
try {
const notification = await getEnvironmentNotification(envId, notifId);
if (!notification) {
return json({ error: 'Environment notification not found' }, { status: 404 });
}
return json(notification);
} catch (error) {
console.error('Error fetching environment notification:', error);
return json({ error: 'Failed to fetch environment notification' }, { status: 500 });
}
};
// PUT /api/environments/[id]/notifications/[notificationId] - Update an environment notification
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('notifications', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const envId = parseInt(params.id);
const notifId = parseInt(params.notificationId);
if (isNaN(envId) || isNaN(notifId)) {
return json({ error: 'Invalid ID' }, { status: 400 });
}
const env = await getEnvironment(envId);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
try {
const body = await request.json();
const { enabled, eventTypes } = body;
const notification = await updateEnvironmentNotification(envId, notifId, {
enabled,
eventTypes: eventTypes as NotificationEventType[]
});
if (!notification) {
return json({ error: 'Environment notification not found' }, { status: 404 });
}
return json(notification);
} catch (error: any) {
console.error('Error updating environment notification:', error);
return json({ error: error.message || 'Failed to update environment notification' }, { status: 500 });
}
};
// DELETE /api/environments/[id]/notifications/[notificationId] - Remove a notification from an environment
export const DELETE: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('notifications', 'delete')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const envId = parseInt(params.id);
const notifId = parseInt(params.notificationId);
if (isNaN(envId) || isNaN(notifId)) {
return json({ error: 'Invalid ID' }, { status: 400 });
}
try {
const deleted = await deleteEnvironmentNotification(envId, notifId);
if (!deleted) {
return json({ error: 'Environment notification not found' }, { status: 404 });
}
return json({ success: true });
} catch (error: any) {
console.error('Error deleting environment notification:', error);
return json({ error: error.message || 'Failed to delete environment notification' }, { status: 500 });
}
};

View File

@@ -0,0 +1,138 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getEnvironment, updateEnvironment } from '$lib/server/db';
import { getDockerInfo } from '$lib/server/docker';
import { edgeConnections, isEdgeConnected } from '$lib/server/hawser';
export const POST: RequestHandler = async ({ params }) => {
try {
const id = parseInt(params.id);
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
// Edge mode - check connection status immediately without blocking
if (env.connectionType === 'hawser-edge') {
const edgeConn = edgeConnections.get(id);
const connected = isEdgeConnected(id);
if (!connected) {
console.log(`[Test] Edge environment ${id} (${env.name}) - agent not connected`);
return json({
success: false,
error: 'Edge agent is not connected',
isEdgeMode: true,
hawser: env.hawserVersion ? {
hawserVersion: env.hawserVersion,
agentId: env.hawserAgentId,
agentName: env.hawserAgentName
} : null
}, { status: 200 });
}
// Agent is connected - try to get Docker info with shorter timeout
console.log(`[Test] Edge environment ${id} (${env.name}) - agent connected, testing Docker...`);
try {
const info = await getDockerInfo(env.id) as any;
return json({
success: true,
info: {
serverVersion: info.ServerVersion,
containers: info.Containers,
images: info.Images,
name: info.Name
},
isEdgeMode: true,
hawser: edgeConn ? {
hawserVersion: edgeConn.agentVersion,
agentId: edgeConn.agentId,
agentName: edgeConn.agentName,
hostname: edgeConn.hostname,
dockerVersion: edgeConn.dockerVersion,
capabilities: edgeConn.capabilities
} : null
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Docker API call failed';
console.error(`[Test] Edge environment ${id} Docker test failed:`, message);
return json({
success: false,
error: message,
isEdgeMode: true,
hawser: edgeConn ? {
hawserVersion: edgeConn.agentVersion,
agentId: edgeConn.agentId,
agentName: edgeConn.agentName
} : null
}, { status: 200 });
}
}
const info = await getDockerInfo(env.id) as any;
// For Hawser Standard mode, fetch Hawser info (Edge mode handled above with early return)
let hawserInfo = null;
if (env.connectionType === 'hawser-standard') {
// Standard mode: fetch via HTTP
try {
const protocol = env.useTls ? 'https' : 'http';
const headers: Record<string, string> = {};
if (env.hawserToken) {
headers['X-Hawser-Token'] = env.hawserToken;
}
const hawserResp = await fetch(`${protocol}://${env.host}:${env.port || 2376}/_hawser/info`, {
headers,
signal: AbortSignal.timeout(5000)
});
if (hawserResp.ok) {
hawserInfo = await hawserResp.json();
// Save hawser info to database
if (hawserInfo?.hawserVersion) {
await updateEnvironment(id, {
hawserVersion: hawserInfo.hawserVersion,
hawserAgentId: hawserInfo.agentId,
hawserAgentName: hawserInfo.agentName,
hawserLastSeen: new Date().toISOString()
});
}
}
} catch {
// Hawser info fetch failed, continue without it
}
}
return json({
success: true,
info: {
serverVersion: info.ServerVersion,
containers: info.Containers,
images: info.Images,
name: info.Name
},
hawser: hawserInfo
});
} catch (error) {
const rawMessage = error instanceof Error ? error.message : 'Connection failed';
console.error('Failed to test connection:', rawMessage);
// Provide more helpful error messages for Hawser connections
let message = rawMessage;
if (rawMessage.includes('401') || rawMessage.toLowerCase().includes('unauthorized')) {
message = 'Invalid token - check that the Hawser token matches';
} else if (rawMessage.includes('403') || rawMessage.toLowerCase().includes('forbidden')) {
message = 'Access forbidden - check token permissions';
} else if (rawMessage.includes('ECONNREFUSED') || rawMessage.includes('Connection refused')) {
message = 'Connection refused - is Hawser running?';
} else if (rawMessage.includes('ETIMEDOUT') || rawMessage.includes('timeout') || rawMessage.includes('Timeout')) {
message = 'Connection timed out - check host and port';
} else if (rawMessage.includes('ENOTFOUND') || rawMessage.includes('getaddrinfo')) {
message = 'Host not found - check the hostname';
} else if (rawMessage.includes('EHOSTUNREACH')) {
message = 'Host unreachable - check network connectivity';
}
return json({ success: false, error: message }, { status: 200 });
}
};

View File

@@ -0,0 +1,75 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import {
getEnvironmentTimezone,
setEnvironmentTimezone,
getEnvironment
} from '$lib/server/db';
import { refreshSchedulesForEnvironment } from '$lib/server/scheduler';
/**
* Get timezone for an environment.
*/
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Verify environment exists
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const timezone = await getEnvironmentTimezone(id);
return json({ timezone });
} catch (error) {
console.error('Failed to get environment timezone:', error);
return json({ error: 'Failed to get environment timezone' }, { status: 500 });
}
};
/**
* Set timezone for an environment.
*/
export const POST: RequestHandler = async ({ params, request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Verify environment exists
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const data = await request.json();
const timezone = data.timezone || 'UTC';
// Validate timezone
const validTimezones = Intl.supportedValuesOf('timeZone');
if (!validTimezones.includes(timezone) && timezone !== 'UTC') {
return json({ error: 'Invalid timezone' }, { status: 400 });
}
await setEnvironmentTimezone(id, timezone);
// Refresh all schedules for this environment to use the new timezone
await refreshSchedulesForEnvironment(id);
return json({ success: true, timezone });
} catch (error) {
console.error('Failed to set environment timezone:', error);
return json({ error: 'Failed to set environment timezone' }, { status: 500 });
}
};

View File

@@ -0,0 +1,87 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import {
getEnvUpdateCheckSettings,
setEnvUpdateCheckSettings,
getEnvironment
} from '$lib/server/db';
import { registerSchedule, unregisterSchedule } from '$lib/server/scheduler';
/**
* Get update check settings for an environment.
*/
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Verify environment exists
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const settings = await getEnvUpdateCheckSettings(id);
return json({
settings: settings || {
enabled: false,
cron: '0 4 * * *',
autoUpdate: false,
vulnerabilityCriteria: 'never'
}
});
} catch (error) {
console.error('Failed to get update check settings:', error);
return json({ error: 'Failed to get update check settings' }, { status: 500 });
}
};
/**
* Save update check settings for an environment.
*/
export const POST: RequestHandler = async ({ params, request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Verify environment exists
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const data = await request.json();
const settings = {
enabled: data.enabled ?? false,
cron: data.cron || '0 4 * * *',
autoUpdate: data.autoUpdate ?? false,
vulnerabilityCriteria: data.vulnerabilityCriteria || 'never'
};
// Save settings to database
await setEnvUpdateCheckSettings(id, settings);
// Register or unregister schedule based on enabled state
if (settings.enabled) {
await registerSchedule(id, 'env_update_check', id);
} else {
unregisterSchedule(id, 'env_update_check');
}
return json({ success: true, settings });
} catch (error) {
console.error('Failed to save update check settings:', error);
return json({ error: 'Failed to save update check settings' }, { status: 500 });
}
};