v1.0.22
This commit is contained in:
1 parent
aefa5e7925
commit
b55e1e5aad
107 files changed
+2193
-443
No files matched your search
@@ -6,7 +6,7 @@ import { getOidcConfig } from '$lib/server/db';
|
||||
// GET /api/auth/oidc/[id]/initiate - Start OIDC authentication flow
|
||||
export const GET: RequestHandler = async ({ params, url }) => {
|
||||
// Check if auth is enabled
|
||||
if (!isAuthEnabled()) {
|
||||
if (!await isAuthEnabled()) {
|
||||
return json({ error: 'Authentication is not enabled' }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
// POST /api/auth/oidc/[id]/initiate - Get authorization URL without redirect
|
||||
export const POST: RequestHandler = async ({ params, request }) => {
|
||||
// Check if auth is enabled
|
||||
if (!isAuthEnabled()) {
|
||||
if (!await isAuthEnabled()) {
|
||||
return json({ error: 'Authentication is not enabled' }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { validateSession, testOidcConnection, isAuthEnabled } from '$lib/server/
|
||||
export const POST: RequestHandler = async ({ params, cookies }) => {
|
||||
// When auth is disabled, allow access (for initial setup)
|
||||
// When auth is enabled, require admin
|
||||
if (isAuthEnabled()) {
|
||||
if (await isAuthEnabled()) {
|
||||
const user = await validateSession(cookies);
|
||||
if (!user || !user.isAdmin) {
|
||||
return json({ error: 'Admin access required' }, { status: 403 });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { auditAuth } from '$lib/server/audit';
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const { url, cookies } = event;
|
||||
// Check if auth is enabled
|
||||
if (!isAuthEnabled()) {
|
||||
if (!await isAuthEnabled()) {
|
||||
throw redirect(302, '/login?error=auth_disabled');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,13 @@ import { deleteAutoUpdateSchedule, getAutoUpdateSetting, removePendingContainerU
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { unregisterSchedule } from '$lib/server/scheduler';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
@@ -41,6 +45,9 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const force = url.searchParams.get('force') === 'true';
|
||||
|
||||
@@ -9,8 +9,12 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { createExec, getDockerConnectionInfo } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request, cookies, url }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !auth.isAuthenticated) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { listContainerDirectory } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path') || '/';
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { chmodContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { readContainerFile, writeContainerFile } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
// Max file size for reading (1MB)
|
||||
const MAX_FILE_SIZE = 1024 * 1024;
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
@@ -60,6 +64,9 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { createContainerFile, createContainerDirectory } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { deleteContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import { getContainerArchive, statContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { renameContainerPath } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { putContainerArchive } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
/**
|
||||
@@ -84,6 +85,9 @@ function createTarArchive(filename: string, content: Uint8Array): Uint8Array {
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async ({ params, url, request, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
|
||||
@@ -2,8 +2,12 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { inspectContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getContainerLogs } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const tail = parseInt(url.searchParams.get('tail') || '100');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getEnvironment } from '$lib/server/db';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import { unixSocketRequest, unixSocketStreamRequest, httpsAgentRequest } from '$lib/server/docker';
|
||||
import type { DockerClientConfig as BaseDockerClientConfig } from '$lib/server/docker';
|
||||
import { sendEdgeRequest, sendEdgeStreamRequest, isEdgeConnected } from '$lib/server/hawser';
|
||||
@@ -254,6 +255,9 @@ async function handleEdgeLogsStream(containerId: string, tail: string, environme
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const containerId = params.id;
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { RequestHandler } from './$types';
|
||||
import { pauseContainer, inspectContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -3,10 +3,14 @@ import { renameContainer, inspectContainer } from '$lib/server/docker';
|
||||
import { renameAutoUpdateSchedule } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, request, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -2,10 +2,14 @@ import { json } from '@sveltejs/kit';
|
||||
import { restartContainer, inspectContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { execInContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
// Shell paths to check
|
||||
@@ -12,6 +13,9 @@ const SHELLS_TO_CHECK = [
|
||||
];
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -2,10 +2,14 @@ import { json } from '@sveltejs/kit';
|
||||
import { startContainer, inspectContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { RequestHandler } from './$types';
|
||||
import { getContainerStats, EnvironmentNotFoundError } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { hasEnvironments } from '$lib/server/db';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
function calculateCpuPercent(stats: any): number {
|
||||
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
||||
@@ -70,6 +71,9 @@ function calculateMemoryUsage(memoryStats: any): { usage: number; raw: number; c
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -2,10 +2,14 @@ import { json } from '@sveltejs/kit';
|
||||
import { stopContainer, inspectContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { execInContainer, getContainerTop } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
function parsePsOutput(output: string): { Titles: string[]; Processes: string[][] } | null {
|
||||
const lines = output.trim().split('\n').filter(line => line.trim());
|
||||
@@ -28,6 +29,9 @@ function parsePsOutput(output: string): { Titles: string[]; Processes: string[][
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { RequestHandler } from './$types';
|
||||
import { unpauseContainer, inspectContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -3,10 +3,14 @@ import { pullImage, updateContainer, type CreateContainerOptions } from '$lib/se
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { removePendingContainerUpdate } from '$lib/server/db';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, request, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'container');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -273,7 +273,6 @@ export const POST: RequestHandler = async (event) => {
|
||||
|
||||
let scanBlocked = false;
|
||||
let blockReason = '';
|
||||
let finalScanResult: ScanResult | undefined;
|
||||
let individualScannerResults: ScannerResult[] = [];
|
||||
|
||||
try {
|
||||
@@ -290,17 +289,7 @@ export const POST: RequestHandler = async (event) => {
|
||||
});
|
||||
|
||||
if (scanResults.length > 0) {
|
||||
const scanSummary = combineScanSummaries(scanResults);
|
||||
finalScanResult = {
|
||||
critical: scanSummary.critical,
|
||||
high: scanSummary.high,
|
||||
medium: scanSummary.medium,
|
||||
low: scanSummary.low,
|
||||
negligible: scanSummary.negligible,
|
||||
unknown: scanSummary.unknown
|
||||
};
|
||||
|
||||
// Build individual scanner results
|
||||
// Build individual scanner results (used by frontend)
|
||||
individualScannerResults = scanResults.map(result => ({
|
||||
scanner: result.scanner as 'grype' | 'trivy',
|
||||
critical: result.summary.critical,
|
||||
@@ -333,8 +322,9 @@ export const POST: RequestHandler = async (event) => {
|
||||
} catch { /* ignore save errors */ }
|
||||
}
|
||||
|
||||
// Check if blocked
|
||||
const { blocked, reason } = shouldBlockUpdate(vulnerabilityCriteria, scanSummary, undefined);
|
||||
// Check if blocked (combineScanSummaries uses Math.max for security check)
|
||||
const combinedForBlockCheck = combineScanSummaries(scanResults);
|
||||
const { blocked, reason } = shouldBlockUpdate(vulnerabilityCriteria, combinedForBlockCheck, undefined);
|
||||
if (blocked) {
|
||||
scanBlocked = true;
|
||||
blockReason = reason;
|
||||
@@ -355,15 +345,21 @@ export const POST: RequestHandler = async (event) => {
|
||||
scanner: v.scanner
|
||||
}));
|
||||
|
||||
// Build scan message from individual results
|
||||
const totalCritical = individualScannerResults.reduce((s, r) => s + r.critical, 0);
|
||||
const totalHigh = individualScannerResults.reduce((s, r) => s + r.high, 0);
|
||||
const totalMedium = individualScannerResults.reduce((s, r) => s + r.medium, 0);
|
||||
const totalLow = individualScannerResults.reduce((s, r) => s + r.low, 0);
|
||||
const hasVulns = totalCritical + totalHigh + totalMedium + totalLow > 0;
|
||||
|
||||
sendData({
|
||||
type: 'scan_complete',
|
||||
containerId,
|
||||
containerName,
|
||||
scanResult: finalScanResult,
|
||||
scannerResults: individualScannerResults.length > 0 ? individualScannerResults : undefined,
|
||||
vulnerabilities: vulnerabilities.length > 0 ? vulnerabilities : undefined,
|
||||
message: finalScanResult
|
||||
? `Scan complete: ${finalScanResult.critical} critical, ${finalScanResult.high} high, ${finalScanResult.medium} medium, ${finalScanResult.low} low`
|
||||
message: hasVulns
|
||||
? `Scan complete: ${totalCritical} critical, ${totalHigh} high, ${totalMedium} medium, ${totalLow} low`
|
||||
: 'Scan complete: no vulnerabilities found'
|
||||
});
|
||||
|
||||
@@ -398,7 +394,6 @@ export const POST: RequestHandler = async (event) => {
|
||||
current: i + 1,
|
||||
total: containerIds.length,
|
||||
success: false,
|
||||
scanResult: finalScanResult,
|
||||
scannerResults: individualScannerResults.length > 0 ? individualScannerResults : undefined,
|
||||
blockReason,
|
||||
message: `Update blocked: ${blockReason}`
|
||||
|
||||
@@ -1,26 +1,59 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { getDashboardPreferences, saveDashboardPreferences } from '$lib/server/db';
|
||||
import { getUserPreference, setUserPreference } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
// Store all dashboard prefs as a single JSON blob to avoid the chained .where() bug
|
||||
// in getUserPreference/setUserPreference (chained .where() replaces instead of ANDing)
|
||||
const DASHBOARD_PREFS_KEY = 'dashboard_prefs';
|
||||
|
||||
interface StoredDashboardPrefs {
|
||||
gridLayout: any[];
|
||||
locked: boolean;
|
||||
viewMode: 'grid' | 'list';
|
||||
}
|
||||
|
||||
async function getPrefs(userId: number | null): Promise<StoredDashboardPrefs> {
|
||||
const stored = await getUserPreference<StoredDashboardPrefs>({
|
||||
userId,
|
||||
environmentId: null,
|
||||
key: DASHBOARD_PREFS_KEY
|
||||
});
|
||||
|
||||
if (stored && typeof stored === 'object' && Array.isArray(stored.gridLayout)) {
|
||||
return {
|
||||
gridLayout: stored.gridLayout,
|
||||
locked: stored.locked ?? false,
|
||||
viewMode: stored.viewMode ?? 'grid'
|
||||
};
|
||||
}
|
||||
|
||||
// Migration: try reading from old dashboard_layout key
|
||||
const oldLayout = await getUserPreference<any[]>({
|
||||
userId,
|
||||
environmentId: null,
|
||||
key: 'dashboard_layout'
|
||||
});
|
||||
|
||||
return {
|
||||
gridLayout: Array.isArray(oldLayout) ? oldLayout : [],
|
||||
locked: false,
|
||||
viewMode: 'grid'
|
||||
};
|
||||
}
|
||||
|
||||
async function savePrefs(userId: number | null, prefs: StoredDashboardPrefs): Promise<void> {
|
||||
await setUserPreference(
|
||||
{ userId, environmentId: null, key: DASHBOARD_PREFS_KEY },
|
||||
prefs
|
||||
);
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
try {
|
||||
// Get user-specific preferences, or fall back to global preferences
|
||||
const userId = auth.user?.id ?? null;
|
||||
const prefs = await getDashboardPreferences(userId);
|
||||
|
||||
// If no preferences exist, return empty gridLayout
|
||||
if (!prefs) {
|
||||
return json({
|
||||
id: 0,
|
||||
userId: null,
|
||||
gridLayout: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const prefs = await getPrefs(userId);
|
||||
return json(prefs);
|
||||
} catch (error) {
|
||||
console.error('Failed to get dashboard preferences:', error);
|
||||
@@ -33,19 +66,23 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { gridLayout } = body;
|
||||
const userId = auth.user?.id ?? null;
|
||||
|
||||
if (!gridLayout || !Array.isArray(gridLayout)) {
|
||||
return json({ error: 'gridLayout is required and must be an array' }, { status: 400 });
|
||||
// Load current prefs and merge changes
|
||||
const current = await getPrefs(userId);
|
||||
|
||||
if (body.gridLayout && Array.isArray(body.gridLayout)) {
|
||||
current.gridLayout = body.gridLayout;
|
||||
}
|
||||
if (body.locked !== undefined) {
|
||||
current.locked = body.locked;
|
||||
}
|
||||
if (body.viewMode !== undefined) {
|
||||
current.viewMode = body.viewMode;
|
||||
}
|
||||
|
||||
const userId = auth.user?.id ?? null;
|
||||
const prefs = await saveDashboardPreferences({
|
||||
userId,
|
||||
gridLayout
|
||||
});
|
||||
|
||||
return json(prefs);
|
||||
await savePrefs(userId, current);
|
||||
return json(current);
|
||||
} catch (error) {
|
||||
console.error('Failed to save dashboard preferences:', error);
|
||||
return json({ error: 'Failed to save dashboard preferences' }, { status: 500 });
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import dependencies from '$lib/data/dependencies.json';
|
||||
import { DEFAULT_GRYPE_IMAGE, DEFAULT_TRIVY_IMAGE } from '$lib/server/scanner';
|
||||
|
||||
// Extract version tag from image string (e.g., "anchore/grype:v0.110.0" -> "v0.110.0")
|
||||
function imageTag(image: string): string {
|
||||
return image.split(':')[1] || 'latest';
|
||||
}
|
||||
|
||||
// External tools used by Dockhand (Docker images)
|
||||
const externalTools = [
|
||||
{
|
||||
name: 'anchore/grype',
|
||||
version: 'latest',
|
||||
version: imageTag(DEFAULT_GRYPE_IMAGE),
|
||||
license: 'Apache-2.0',
|
||||
repository: 'https://github.com/anchore/grype'
|
||||
},
|
||||
{
|
||||
name: 'aquasec/trivy',
|
||||
version: 'latest',
|
||||
version: imageTag(DEFAULT_TRIVY_IMAGE),
|
||||
license: 'Apache-2.0',
|
||||
repository: 'https://github.com/aquasecurity/trivy'
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { cleanPem } from '$lib/utils/pem';
|
||||
import { unregisterSchedule } from '$lib/server/scheduler';
|
||||
import { closeEdgeConnection } from '$lib/server/hawser';
|
||||
import { computeAuditDiff } from '$lib/utils/diff';
|
||||
import { deleteEnvironmentIcon } from '$lib/server/env-icons';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
@@ -167,6 +168,9 @@ export const DELETE: RequestHandler = async (event) => {
|
||||
return json({ error: 'Cannot delete this environment' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Clean up custom icon file if exists
|
||||
deleteEnvironmentIcon(id);
|
||||
|
||||
// Clean up public IP entry for this environment
|
||||
await deleteEnvironmentPublicIp(id);
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getEnvironment, updateEnvironment } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { saveEnvironmentIcon, deleteEnvironmentIcon, getEnvironmentIconBuffer } from '$lib/server/env-icons';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const id = parseInt(params.id);
|
||||
const buffer = getEnvironmentIconBuffer(id);
|
||||
|
||||
if (!buffer) {
|
||||
return json({ error: 'No custom icon' }, { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(new Uint8Array(buffer), {
|
||||
headers: {
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id);
|
||||
const env = await getEnvironment(id);
|
||||
if (!env) {
|
||||
return json({ error: 'Environment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
if (!data.image || typeof data.image !== 'string') {
|
||||
return json({ error: 'Missing image data' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate size (~200KB base64 limit)
|
||||
if (data.image.length > 300_000) {
|
||||
return json({ error: 'Image too large' }, { status: 400 });
|
||||
}
|
||||
|
||||
saveEnvironmentIcon(id, data.image);
|
||||
const iconValue = `custom:env-${id}.webp`;
|
||||
await updateEnvironment(id, { icon: iconValue });
|
||||
|
||||
return json({ success: true, icon: iconValue });
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('environments', 'edit')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id);
|
||||
const env = await getEnvironment(id);
|
||||
if (!env) {
|
||||
return json({ error: 'Environment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
deleteEnvironmentIcon(id);
|
||||
await updateEnvironment(id, { icon: 'globe' });
|
||||
|
||||
return json({ success: true, icon: 'globe' });
|
||||
};
|
||||
@@ -13,7 +13,23 @@ const TIMEZONE_ALIASES: Record<string, string> = {
|
||||
'Europe/Kyiv': 'Europe/Kiev',
|
||||
'Asia/Ho_Chi_Minh': 'Asia/Saigon',
|
||||
'America/Nuuk': 'America/Godthab',
|
||||
'Pacific/Kanton': 'Pacific/Enderbury'
|
||||
'Pacific/Kanton': 'Pacific/Enderbury',
|
||||
// Modern IANA names that Node.js ICU maps to legacy names
|
||||
'Asia/Kolkata': 'Asia/Calcutta',
|
||||
'Asia/Kathmandu': 'Asia/Katmandu',
|
||||
'Asia/Yangon': 'Asia/Rangoon',
|
||||
'Asia/Kashgar': 'Asia/Urumqi',
|
||||
'Atlantic/Faroe': 'Atlantic/Faeroe',
|
||||
'Europe/Uzhgorod': 'Europe/Kiev',
|
||||
'Europe/Zaporozhye': 'Europe/Kiev',
|
||||
'America/Atikokan': 'America/Coral_Harbour',
|
||||
'America/Argentina/Buenos_Aires': 'America/Buenos_Aires',
|
||||
'America/Argentina/Catamarca': 'America/Catamarca',
|
||||
'America/Argentina/Cordoba': 'America/Cordoba',
|
||||
'America/Argentina/Jujuy': 'America/Jujuy',
|
||||
'America/Argentina/Mendoza': 'America/Mendoza',
|
||||
'Pacific/Pohnpei': 'Pacific/Ponape',
|
||||
'Pacific/Chuuk': 'Pacific/Truk'
|
||||
};
|
||||
|
||||
function normalizeTimezone(tz: string): string {
|
||||
|
||||
@@ -4,9 +4,10 @@ import {
|
||||
getGitRepository,
|
||||
updateGitRepository,
|
||||
deleteGitRepository,
|
||||
getGitCredentials
|
||||
getGitCredentials,
|
||||
getGitStacksByRepositoryId
|
||||
} from '$lib/server/db';
|
||||
import { deleteRepositoryFiles } from '$lib/server/git';
|
||||
import { deleteRepositoryFiles, deleteGitStackFiles } from '$lib/server/git';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditGitRepository } from '$lib/server/audit';
|
||||
import { computeAuditDiff } from '$lib/utils/diff';
|
||||
@@ -112,7 +113,13 @@ export const DELETE: RequestHandler = async (event) => {
|
||||
return json({ error: 'Repository not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete repository files first
|
||||
// Delete git stack clone directories before cascade deletes the DB rows
|
||||
const stacks = await getGitStacksByRepositoryId(id);
|
||||
for (const stack of stacks) {
|
||||
await deleteGitStackFiles(stack.id, stack.stackName, stack.environmentId);
|
||||
}
|
||||
|
||||
// Delete repository clone directory
|
||||
deleteRepositoryFiles(id);
|
||||
|
||||
const deleted = await deleteGitRepository(id);
|
||||
|
||||
@@ -17,10 +17,10 @@ function verifySignature(payload: string, signature: string | null, secret: stri
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature)
|
||||
);
|
||||
const sigBuf = Buffer.from(signature);
|
||||
const expectedBuf = Buffer.from(expectedSignature);
|
||||
if (sigBuf.length !== expectedBuf.length) return false;
|
||||
return crypto.timingSafeEqual(sigBuf, expectedBuf);
|
||||
}
|
||||
|
||||
// GitLab uses X-Gitlab-Token which should match exactly
|
||||
|
||||
@@ -17,10 +17,10 @@ function verifySignature(payload: string, signature: string | null, secret: stri
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature)
|
||||
);
|
||||
const sigBuf = Buffer.from(signature);
|
||||
const expectedBuf = Buffer.from(expectedSignature);
|
||||
if (sigBuf.length !== expectedBuf.length) return false;
|
||||
return crypto.timingSafeEqual(sigBuf, expectedBuf);
|
||||
}
|
||||
|
||||
// GitLab uses X-Gitlab-Token which should match exactly
|
||||
|
||||
@@ -2,10 +2,14 @@ import { json } from '@sveltejs/kit';
|
||||
import { removeImage, inspectImage } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditImage } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'image');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const force = url.searchParams.get('force') === 'true';
|
||||
|
||||
@@ -3,9 +3,13 @@ import { exportImage, inspectImage } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { createGzip } from 'zlib';
|
||||
import { Readable } from 'stream';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'image');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -2,8 +2,12 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getImageHistory } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'image');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { tagImage } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'image');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -10,7 +10,9 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
|
||||
// Return as plain text if requested
|
||||
if (url.searchParams.get('format') === 'text') {
|
||||
return text(content);
|
||||
return text(content, {
|
||||
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
||||
});
|
||||
}
|
||||
|
||||
return json({ content });
|
||||
|
||||
@@ -10,7 +10,9 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
|
||||
// Return as plain text if requested
|
||||
if (url.searchParams.get('format') === 'text') {
|
||||
return text(content);
|
||||
return text(content, {
|
||||
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
||||
});
|
||||
}
|
||||
|
||||
return json({ content });
|
||||
|
||||
@@ -3,8 +3,12 @@ import type { RequestHandler } from './$types';
|
||||
import { removeNetwork, inspectNetwork } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditNetwork } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'network');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
@@ -32,6 +36,9 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'network');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { RequestHandler } from './$types';
|
||||
import { connectContainerToNetwork, inspectNetwork } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditNetwork } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, request, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'network');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
@@ -25,6 +29,9 @@ export const POST: RequestHandler = async (event) => {
|
||||
return json({ error: 'Container ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const invalidContainer = validateDockerIdParam(containerId, 'container');
|
||||
if (invalidContainer) return invalidContainer;
|
||||
|
||||
// Get network name for audit
|
||||
let networkName = params.id;
|
||||
try {
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { RequestHandler } from './$types';
|
||||
import { disconnectContainerFromNetwork, inspectNetwork } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditNetwork } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, request, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.id, 'network');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
@@ -25,6 +29,9 @@ export const POST: RequestHandler = async (event) => {
|
||||
return json({ error: 'Container ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const invalidContainer = validateDockerIdParam(containerId, 'container');
|
||||
if (invalidContainer) return invalidContainer;
|
||||
|
||||
// Get network name for audit
|
||||
let networkName = params.id;
|
||||
try {
|
||||
|
||||
@@ -2,8 +2,12 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { inspectNetwork } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.id, 'network');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -371,19 +371,24 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
...networkEnvVars
|
||||
];
|
||||
|
||||
// Pass Docker API version so the updater CLI speaks a compatible version.
|
||||
// Without this, newer CLI versions (e.g. API 1.53) fail against older
|
||||
// daemons (e.g. Synology DSM shipping API 1.43).
|
||||
const dockerApiVersion = process.env.DOCKER_API_VERSION;
|
||||
if (dockerApiVersion) {
|
||||
updaterEnv.push(`DOCKER_API_VERSION=${dockerApiVersion}`);
|
||||
// Pin Docker API version so the updater's bundled Docker CLI
|
||||
// doesn't request a version newer than the host daemon supports
|
||||
// (e.g. Synology DSM with Docker 24.x / API 1.43)
|
||||
if (process.env.DOCKER_API_VERSION) {
|
||||
updaterEnv.push(`DOCKER_API_VERSION=${process.env.DOCKER_API_VERSION}`);
|
||||
console.log(`[SelfUpdate] Forwarding explicit DOCKER_API_VERSION: ${process.env.DOCKER_API_VERSION}`);
|
||||
} else {
|
||||
const versionRes = await localDockerFetch('/version');
|
||||
if (versionRes.ok) {
|
||||
const vInfo = await versionRes.json() as { ApiVersion?: string };
|
||||
if (vInfo.ApiVersion) {
|
||||
updaterEnv.push(`DOCKER_API_VERSION=${vInfo.ApiVersion}`);
|
||||
try {
|
||||
const versionResp = await localDockerFetch('/version');
|
||||
if (versionResp.ok) {
|
||||
const versionInfo = await versionResp.json() as { ApiVersion?: string };
|
||||
if (versionInfo.ApiVersion) {
|
||||
updaterEnv.push(`DOCKER_API_VERSION=${versionInfo.ApiVersion}`);
|
||||
console.log(`[SelfUpdate] Using negotiated Docker API version: ${versionInfo.ApiVersion}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.warn('[SelfUpdate] Could not detect Docker API version, updater will negotiate on its own');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
|
||||
const containerId = getOwnContainerId();
|
||||
if (!containerId) {
|
||||
console.log('[SelfUpdate] Not running in Docker, skipping update check');
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
error: 'Not running in Docker'
|
||||
@@ -45,6 +46,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
// Inspect own container to get current image info
|
||||
const inspectResponse = await localDockerFetch(`/containers/${containerId}/json`);
|
||||
if (!inspectResponse.ok) {
|
||||
console.log(`[SelfUpdate] Failed to inspect container ${containerId.substring(0, 12)}: ${inspectResponse.status}`);
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
error: 'Failed to inspect own container'
|
||||
@@ -61,7 +63,10 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const currentImageId = inspectData.Image || '';
|
||||
const containerName = inspectData.Name?.replace(/^\//, '') || '';
|
||||
|
||||
console.log(`[SelfUpdate] Container: ${containerId.substring(0, 12)}, image: ${currentImage}, tag: ${currentImage.split(':').pop() || 'latest'}`);
|
||||
|
||||
if (!currentImage) {
|
||||
console.log('[SelfUpdate] Could not determine current image from inspect data');
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
error: 'Could not determine current image'
|
||||
@@ -73,6 +78,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
|
||||
// Digest-based images (e.g. image@sha256:...) can't be checked for updates
|
||||
if (currentImage.includes('@sha256:')) {
|
||||
console.log('[SelfUpdate] Image pinned by digest, cannot check for updates');
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -94,6 +100,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
// Version-based check: compare against latest released version from changelog
|
||||
const currentTagVersion = versionMatch[1];
|
||||
const suffix = versionMatch[2] || ''; // '-baseline' or ''
|
||||
console.log(`[SelfUpdate] Version-based check: current=${currentTagVersion}${suffix}`);
|
||||
|
||||
try {
|
||||
const changelogResponse = await fetch(
|
||||
@@ -102,6 +109,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
);
|
||||
|
||||
if (!changelogResponse.ok) {
|
||||
console.log(`[SelfUpdate] Failed to fetch changelog from GitHub: ${changelogResponse.status}`);
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -122,6 +130,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const latestRelease = changelog.find(entry => !entry.comingSoon);
|
||||
|
||||
if (!latestRelease) {
|
||||
console.log('[SelfUpdate] No released version found in changelog');
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -133,12 +142,14 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
|
||||
const latestVersion = latestRelease.version;
|
||||
const hasNewer = compareVersions(latestVersion, currentTagVersion) > 0;
|
||||
console.log(`[SelfUpdate] Latest changelog version: ${latestVersion}, current: ${currentTagVersion}, hasNewer: ${hasNewer}`);
|
||||
|
||||
if (hasNewer) {
|
||||
// Build new image tag preserving registry prefix and suffix
|
||||
const newTag = `v${latestVersion.replace(/^v/, '')}${suffix}`;
|
||||
const newImage = `${imageWithoutTag}:${newTag}`;
|
||||
|
||||
console.log(`[SelfUpdate] Update available: ${currentImage} → ${newImage}`);
|
||||
return json({
|
||||
updateAvailable: true,
|
||||
currentImage,
|
||||
@@ -149,6 +160,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[SelfUpdate] Up to date (version ${currentTagVersion})`);
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -156,6 +168,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
isComposeManaged
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(`[SelfUpdate] Version check failed: ${err}`);
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -167,10 +180,12 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
}
|
||||
|
||||
// Digest-based check for mutable tags (:latest, :baseline, etc.)
|
||||
console.log(`[SelfUpdate] Digest-based check for mutable tag: ${tag}`);
|
||||
|
||||
// Inspect image via local Docker socket to get RepoDigests
|
||||
const imageResponse = await localDockerFetch(`/images/${encodeURIComponent(currentImageId)}/json`);
|
||||
if (!imageResponse.ok) {
|
||||
console.log(`[SelfUpdate] Failed to inspect image ${currentImageId}: ${imageResponse.status}`);
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -192,6 +207,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (localDigests.length === 0) {
|
||||
console.log('[SelfUpdate] No RepoDigests found — local/untagged image, cannot check registry');
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -202,9 +218,12 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[SelfUpdate] Local digests: ${localDigests.map(d => d.substring(0, 19)).join(', ')}`);
|
||||
|
||||
// Query registry for latest digest
|
||||
const registryDigest = await getRegistryManifestDigest(currentImage);
|
||||
if (!registryDigest) {
|
||||
console.log(`[SelfUpdate] Could not query registry for ${currentImage}`);
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
currentImage,
|
||||
@@ -216,6 +235,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
}
|
||||
|
||||
const hasUpdate = !localDigests.includes(registryDigest);
|
||||
console.log(`[SelfUpdate] Registry digest: ${registryDigest.substring(0, 19)}, match: ${!hasUpdate}, updateAvailable: ${hasUpdate}`);
|
||||
|
||||
return json({
|
||||
updateAvailable: hasUpdate,
|
||||
@@ -227,6 +247,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
isComposeManaged
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(`[SelfUpdate] Check failed with error: ${err}`);
|
||||
return json({
|
||||
updateAvailable: false,
|
||||
error: 'Check failed: ' + String(err)
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { refreshSystemJobs } from '$lib/server/scheduler';
|
||||
import { sendToEventSubprocess, sendToMetricsSubprocess } from '$lib/server/subprocess-manager';
|
||||
import { DEFAULT_GRYPE_IMAGE, DEFAULT_TRIVY_IMAGE } from '$lib/server/scanner';
|
||||
|
||||
export type TimeFormat = '12h' | '24h';
|
||||
export type DateFormat = 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'DD.MM.YYYY';
|
||||
@@ -67,10 +68,15 @@ export interface GeneralSettings {
|
||||
editorFont: string;
|
||||
// Compact ports
|
||||
compactPorts: boolean;
|
||||
// Log timestamp formatting
|
||||
formatLogTimestamps: boolean;
|
||||
// External stack paths
|
||||
externalStackPaths: string[];
|
||||
// Primary stack location
|
||||
primaryStackLocation: string | null;
|
||||
// Scanner images
|
||||
defaultGrypeImage: string;
|
||||
defaultTrivyImage: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRetentionDays' | 'scheduleCleanupCron' | 'eventCleanupCron' | 'scheduleCleanupEnabled' | 'eventCleanupEnabled'> = {
|
||||
@@ -88,13 +94,18 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
|
||||
eventPollInterval: 60000,
|
||||
metricsCollectionInterval: 30000,
|
||||
compactPorts: false,
|
||||
formatLogTimestamps: false,
|
||||
lightTheme: 'default',
|
||||
darkTheme: 'default',
|
||||
font: 'system',
|
||||
fontSize: 'normal',
|
||||
gridFontSize: 'normal',
|
||||
terminalFont: 'system-mono',
|
||||
editorFont: 'system-mono'
|
||||
editorFont: 'system-mono',
|
||||
externalStackPaths: [],
|
||||
primaryStackLocation: null,
|
||||
defaultGrypeImage: DEFAULT_GRYPE_IMAGE,
|
||||
defaultTrivyImage: DEFAULT_TRIVY_IMAGE
|
||||
};
|
||||
|
||||
const VALID_LIGHT_THEMES = ['default', 'catppuccin', 'rose-pine', 'nord', 'solarized', 'gruvbox', 'alucard', 'github', 'material', 'atom-one'];
|
||||
@@ -144,8 +155,11 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
terminalFont,
|
||||
editorFont,
|
||||
compactPorts,
|
||||
formatLogTimestamps,
|
||||
externalStackPaths,
|
||||
primaryStackLocation
|
||||
primaryStackLocation,
|
||||
defaultGrypeImage,
|
||||
defaultTrivyImage
|
||||
] = await Promise.all([
|
||||
getSetting('confirm_destructive'),
|
||||
getSetting('show_stopped_containers'),
|
||||
@@ -174,8 +188,11 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
getSetting('theme_terminal_font'),
|
||||
getSetting('theme_editor_font'),
|
||||
getSetting('compact_ports'),
|
||||
getSetting('format_log_timestamps'),
|
||||
getExternalStackPaths(),
|
||||
getPrimaryStackLocation()
|
||||
getPrimaryStackLocation(),
|
||||
getSetting('default_grype_image'),
|
||||
getSetting('default_trivy_image')
|
||||
]);
|
||||
|
||||
const settings: GeneralSettings = {
|
||||
@@ -206,8 +223,11 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
terminalFont: terminalFont ?? DEFAULT_SETTINGS.terminalFont,
|
||||
editorFont: editorFont ?? DEFAULT_SETTINGS.editorFont,
|
||||
compactPorts: compactPorts ?? DEFAULT_SETTINGS.compactPorts,
|
||||
formatLogTimestamps: formatLogTimestamps ?? DEFAULT_SETTINGS.formatLogTimestamps,
|
||||
externalStackPaths,
|
||||
primaryStackLocation
|
||||
primaryStackLocation,
|
||||
defaultGrypeImage: defaultGrypeImage ?? DEFAULT_GRYPE_IMAGE,
|
||||
defaultTrivyImage: defaultTrivyImage ?? DEFAULT_TRIVY_IMAGE
|
||||
};
|
||||
|
||||
return json(settings);
|
||||
@@ -225,7 +245,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, externalStackPaths, primaryStackLocation } = body;
|
||||
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage } = body;
|
||||
|
||||
if (confirmDestructive !== undefined) {
|
||||
await setSetting('confirm_destructive', confirmDestructive);
|
||||
@@ -321,6 +341,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
if (compactPorts !== undefined) {
|
||||
await setSetting('compact_ports', compactPorts);
|
||||
}
|
||||
if (formatLogTimestamps !== undefined) {
|
||||
await setSetting('format_log_timestamps', formatLogTimestamps);
|
||||
}
|
||||
if (externalStackPaths !== undefined && Array.isArray(externalStackPaths)) {
|
||||
// Filter to valid non-empty strings
|
||||
const validPaths = externalStackPaths.filter((p: unknown) => typeof p === 'string' && p.trim());
|
||||
@@ -335,6 +358,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
await setPrimaryStackLocation(null);
|
||||
}
|
||||
}
|
||||
if (defaultGrypeImage !== undefined && typeof defaultGrypeImage === 'string') {
|
||||
await setSetting('default_grype_image', defaultGrypeImage);
|
||||
}
|
||||
if (defaultTrivyImage !== undefined && typeof defaultTrivyImage === 'string') {
|
||||
await setSetting('default_trivy_image', defaultTrivyImage);
|
||||
}
|
||||
|
||||
// Fetch all settings in parallel for the response
|
||||
const [
|
||||
@@ -365,8 +394,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
terminalFontVal,
|
||||
editorFontVal,
|
||||
compactPortsVal,
|
||||
formatLogTimestampsVal,
|
||||
externalStackPathsVal,
|
||||
primaryStackLocationVal
|
||||
primaryStackLocationVal,
|
||||
defaultGrypeImageVal,
|
||||
defaultTrivyImageVal
|
||||
] = await Promise.all([
|
||||
getSetting('confirm_destructive'),
|
||||
getSetting('show_stopped_containers'),
|
||||
@@ -395,8 +427,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
getSetting('theme_terminal_font'),
|
||||
getSetting('theme_editor_font'),
|
||||
getSetting('compact_ports'),
|
||||
getSetting('format_log_timestamps'),
|
||||
getExternalStackPaths(),
|
||||
getPrimaryStackLocation()
|
||||
getPrimaryStackLocation(),
|
||||
getSetting('default_grype_image'),
|
||||
getSetting('default_trivy_image')
|
||||
]);
|
||||
|
||||
const settings: GeneralSettings = {
|
||||
@@ -427,8 +462,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
terminalFont: terminalFontVal ?? DEFAULT_SETTINGS.terminalFont,
|
||||
editorFont: editorFontVal ?? DEFAULT_SETTINGS.editorFont,
|
||||
compactPorts: compactPortsVal ?? DEFAULT_SETTINGS.compactPorts,
|
||||
formatLogTimestamps: formatLogTimestampsVal ?? DEFAULT_SETTINGS.formatLogTimestamps,
|
||||
externalStackPaths: externalStackPathsVal,
|
||||
primaryStackLocation: primaryStackLocationVal
|
||||
primaryStackLocation: primaryStackLocationVal,
|
||||
defaultGrypeImage: defaultGrypeImageVal ?? DEFAULT_GRYPE_IMAGE,
|
||||
defaultTrivyImage: defaultTrivyImageVal ?? DEFAULT_TRIVY_IMAGE
|
||||
};
|
||||
|
||||
return json(settings);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { getEnvSetting, setEnvSetting, getEnvironment } from '$lib/server/db';
|
||||
import { getEnvSetting, setEnvSetting, getEnvironment, setSetting } from '$lib/server/db';
|
||||
import {
|
||||
checkScannerAvailability,
|
||||
getScannerVersions,
|
||||
@@ -15,6 +15,8 @@ export interface ScannerSettings {
|
||||
scanner: ScannerType;
|
||||
grypeArgs: string;
|
||||
trivyArgs: string;
|
||||
grypeImage: string;
|
||||
trivyImage: string;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
@@ -39,7 +41,9 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
const settings: ScannerSettings = {
|
||||
scanner: await getEnvSetting('vulnerability_scanner', parsedEnvId) || 'none',
|
||||
grypeArgs: await getEnvSetting('grype_cli_args', parsedEnvId) || globalDefaults.grypeArgs,
|
||||
trivyArgs: await getEnvSetting('trivy_cli_args', parsedEnvId) || globalDefaults.trivyArgs
|
||||
trivyArgs: await getEnvSetting('trivy_cli_args', parsedEnvId) || globalDefaults.trivyArgs,
|
||||
grypeImage: globalDefaults.grypeImage,
|
||||
trivyImage: globalDefaults.trivyImage
|
||||
};
|
||||
|
||||
// Fast path: return just settings without Docker checks
|
||||
@@ -80,7 +84,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { scanner, grypeArgs, trivyArgs, envId } = body;
|
||||
const { scanner, grypeArgs, trivyArgs, grypeImage, trivyImage, envId } = body;
|
||||
const parsedEnvId = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
@@ -104,6 +108,12 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
|
||||
if (trivyArgs !== undefined) {
|
||||
await setEnvSetting('trivy_cli_args', trivyArgs, parsedEnvId);
|
||||
}
|
||||
if (grypeImage !== undefined && typeof grypeImage === 'string') {
|
||||
await setSetting('default_grype_image', grypeImage);
|
||||
}
|
||||
if (trivyImage !== undefined && typeof trivyImage === 'string') {
|
||||
await setSetting('default_trivy_image', trivyImage);
|
||||
}
|
||||
|
||||
// Get global defaults for fallback
|
||||
const globalDefaults = await getGlobalScannerDefaults();
|
||||
@@ -113,7 +123,9 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
|
||||
settings: {
|
||||
scanner: await getEnvSetting('vulnerability_scanner', parsedEnvId) || 'none',
|
||||
grypeArgs: await getEnvSetting('grype_cli_args', parsedEnvId) || globalDefaults.grypeArgs,
|
||||
trivyArgs: await getEnvSetting('trivy_cli_args', parsedEnvId) || globalDefaults.trivyArgs
|
||||
trivyArgs: await getEnvSetting('trivy_cli_args', parsedEnvId) || globalDefaults.trivyArgs,
|
||||
grypeImage: globalDefaults.grypeImage,
|
||||
trivyImage: globalDefaults.trivyImage
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -154,6 +166,9 @@ export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||
const removed: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
// Get configured scanner images
|
||||
const globalDefaults = await getGlobalScannerDefaults();
|
||||
|
||||
// Determine which images to remove
|
||||
const scannersToRemove: ('grype' | 'trivy')[] =
|
||||
scanner === 'grype' ? ['grype'] :
|
||||
@@ -161,7 +176,7 @@ export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||
['grype', 'trivy'];
|
||||
|
||||
for (const scannerType of scannersToRemove) {
|
||||
const imageName = scannerType === 'grype' ? 'anchore/grype' : 'aquasec/trivy';
|
||||
const imageName = scannerType === 'grype' ? globalDefaults.grypeImage.split(':')[0] : globalDefaults.trivyImage.split(':')[0];
|
||||
|
||||
// Find the image
|
||||
const image = images.find((img) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { readdirSync, statSync, existsSync } from 'node:fs';
|
||||
import { join, basename } from 'node:path';
|
||||
import { readdirSync, statSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { join, basename, isAbsolute } from 'node:path';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
export interface FileEntry {
|
||||
@@ -13,6 +13,49 @@ export interface FileEntry {
|
||||
mode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/system/files
|
||||
* Create a directory
|
||||
*
|
||||
* Body: { path: string }
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
if (auth.authEnabled && !await auth.can('stacks', 'edit')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const path = body.path;
|
||||
|
||||
if (!path || typeof path !== 'string') {
|
||||
return json({ error: 'Path is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!isAbsolute(path)) {
|
||||
return json({ error: 'Path must be absolute' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (path.includes('..')) {
|
||||
return json({ error: 'Path must not contain ..' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (existsSync(path)) {
|
||||
return json({ error: 'Path already exists' }, { status: 409 });
|
||||
}
|
||||
|
||||
mkdirSync(path, { recursive: true });
|
||||
|
||||
return json({ success: true, path });
|
||||
} catch (error) {
|
||||
console.error('Error creating directory:', error);
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
return json({ error: `Failed to create directory: ${message}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/system/files
|
||||
* Browse Dockhand's local filesystem (for mount browsing)
|
||||
|
||||
@@ -3,8 +3,12 @@ import type { RequestHandler } from './$types';
|
||||
import { removeVolume, inspectVolume } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditVolume } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
@@ -33,6 +37,9 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const force = url.searchParams.get('force') === 'true';
|
||||
|
||||
@@ -2,8 +2,12 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { listVolumeDirectory, getVolumeUsage } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -2,11 +2,15 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { readVolumeFile } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
// Max file size for reading (1MB)
|
||||
const MAX_FILE_SIZE = 1024 * 1024;
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const path = url.searchParams.get('path');
|
||||
|
||||
@@ -2,12 +2,16 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { releaseVolumeHelperContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
/**
|
||||
* Release the cached volume helper container when done browsing.
|
||||
* This is called when the volume browser modal is closed.
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { inspectVolume, createVolume, type CreateVolumeOptions } from '$lib/server/docker';
|
||||
import { inspectVolume, createVolume, type CreateVolumeOptions, ensureVolumeHelperImage, dockerFetch, dockerJsonRequest, drainResponse } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditVolume } from '$lib/server/audit';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, request, cookies } = event;
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
@@ -38,6 +42,49 @@ export const POST: RequestHandler = async (event) => {
|
||||
|
||||
const newVolume = await createVolume(options, envIdNum);
|
||||
|
||||
// Copy data from source to destination using a temporary busybox container
|
||||
// Mount source read-only at /src and destination read-write at /dst
|
||||
await ensureVolumeHelperImage(envIdNum);
|
||||
|
||||
const containerName = `dockhand-clone-${Date.now().toString(36)}`;
|
||||
const containerConfig = {
|
||||
Image: 'busybox:latest',
|
||||
Cmd: ['cp', '-a', '/src/.', '/dst/'],
|
||||
HostConfig: {
|
||||
Binds: [
|
||||
`${params.name}:/src:ro`,
|
||||
`${newName}:/dst`
|
||||
],
|
||||
AutoRemove: false
|
||||
},
|
||||
Labels: { 'dockhand.volume.helper': 'true' }
|
||||
};
|
||||
|
||||
let copyCtrId: string | undefined;
|
||||
try {
|
||||
const createRes = await dockerJsonRequest<{ Id: string }>(
|
||||
`/containers/create?name=${encodeURIComponent(containerName)}`,
|
||||
{ method: 'POST', body: JSON.stringify(containerConfig) },
|
||||
envIdNum
|
||||
);
|
||||
copyCtrId = createRes.Id;
|
||||
|
||||
await drainResponse(await dockerFetch(`/containers/${copyCtrId}/start`, { method: 'POST' }, envIdNum));
|
||||
|
||||
// Wait for the copy to finish (must drain response to ensure wait completes)
|
||||
const waitRes = await dockerFetch(`/containers/${copyCtrId}/wait`, { method: 'POST' }, envIdNum);
|
||||
const waitBody = await waitRes.json().catch(() => ({ StatusCode: -1 }));
|
||||
if (waitBody.StatusCode !== 0) {
|
||||
throw new Error(`Volume copy failed with exit code ${waitBody.StatusCode}`);
|
||||
}
|
||||
} finally {
|
||||
if (copyCtrId) {
|
||||
await drainResponse(
|
||||
await dockerFetch(`/containers/${copyCtrId}?force=true`, { method: 'DELETE' }, envIdNum)
|
||||
).catch(() => { /* best effort cleanup */ });
|
||||
}
|
||||
}
|
||||
|
||||
// Audit log
|
||||
await auditVolume(event, 'clone', newVolume.Name, `${params.name} → ${newName}`, envIdNum, {
|
||||
source: params.name,
|
||||
|
||||
@@ -3,8 +3,12 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getVolumeArchive } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
@@ -2,8 +2,12 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { inspectVolume } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { validateDockerIdParam } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const invalid = validateDockerIdParam(params.name, 'volume');
|
||||
if (invalid) return invalid;
|
||||
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
|
||||
Reference in new issue
Block a user