This commit is contained in:
jarek committed 2026-01-14 08:18:20 +01:00
1 parent 027aee434c
commit 62636426bf
15 files changed
+317 -65

No files matched your search

+13 -1
View File
@@ -12,7 +12,7 @@
import { SidebarProvider, SidebarTrigger } from '$lib/components/ui/sidebar';
import { startStatsCollection, stopStatsCollection } from '$lib/stores/stats';
import { connectSSE, disconnectSSE } from '$lib/stores/events';
import { currentEnvironment } from '$lib/stores/environment';
import { currentEnvironment, environments } from '$lib/stores/environment';
import { licenseStore, daysUntilExpiry } from '$lib/stores/license';
import { authStore } from '$lib/stores/auth';
import { themeStore, applyTheme } from '$lib/stores/theme';
@@ -51,6 +51,18 @@
}
});
// Refresh environments when user becomes authenticated
// This handles OIDC callback where login happens server-side
let wasAuthenticated = $state(false);
$effect(() => {
if (!$authStore.loading && $authStore.authenticated && !wasAuthenticated) {
environments.refresh();
}
if (!$authStore.loading) {
wasAuthenticated = $authStore.authenticated;
}
});
onMount(() => {
// Apply theme from localStorage immediately (for flash-free loading)
applyTheme(themeStore.get());
+1 -1
View File
@@ -12,7 +12,7 @@
import DraggableGrid, { type GridItemLayout } from './dashboard/DraggableGrid.svelte';
import { dashboardPreferences, dashboardData, GRID_COLS, GRID_ROW_HEIGHT, type TileItem } from '$lib/stores/dashboard';
import { currentEnvironment } from '$lib/stores/environment';
import { IsMobile } from '$lib/hooks/is-mobile.svelte.js';
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
import type { EnvironmentStats } from './api/dashboard/stats/+server';
import { getLabelColor, getLabelBgColor } from '$lib/utils/label-colors';
+10 -2
View File
@@ -13,6 +13,9 @@ import { authorize } from '$lib/server/authorize';
import { registerSchedule } from '$lib/server/scheduler';
import { secureRandomBytes } from '$lib/server/crypto-fallback';
// Stack name validation: must start with alphanumeric, can contain alphanumeric, hyphens, underscores
const STACK_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
@@ -49,6 +52,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
return json({ error: 'Stack name is required' }, { status: 400 });
}
const trimmedStackName = data.stackName.trim();
if (!STACK_NAME_REGEX.test(trimmedStackName)) {
return json({ error: 'Stack name must start with a letter or number, and contain only letters, numbers, hyphens, and underscores' }, { status: 400 });
}
// Either repositoryId or new repo details (url, branch) must be provided
let repositoryId = data.repositoryId;
@@ -98,7 +106,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
}
const gitStack = await createGitStack({
stackName: data.stackName,
stackName: trimmedStackName,
environmentId: data.environmentId || null,
repositoryId: repositoryId,
composePath: data.composePath || 'docker-compose.yml',
@@ -112,7 +120,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
// Create stack_sources entry so the stack appears in the list immediately
await upsertStackSource({
stackName: data.stackName,
stackName: trimmedStackName,
environmentId: data.environmentId || null,
sourceType: 'git',
gitRepositoryId: repositoryId,
+26 -1
View File
@@ -1,10 +1,13 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getGitStack, updateGitStack, deleteGitStack } from '$lib/server/db';
import { getGitStack, updateGitStack, deleteGitStack, deleteStackSource, updateStackSourceName } from '$lib/server/db';
import { deleteGitStackFiles, deployGitStack } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
import { registerSchedule, unregisterSchedule } from '$lib/server/scheduler';
// Stack name validation: must start with alphanumeric, can contain alphanumeric, hyphens, underscores
const STACK_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
@@ -43,6 +46,20 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
}
const data = await request.json();
// Validate stack name if it's being changed
if (data.stackName !== undefined) {
const trimmedStackName = data.stackName.trim();
if (!trimmedStackName) {
return json({ error: 'Stack name is required' }, { status: 400 });
}
if (!STACK_NAME_REGEX.test(trimmedStackName)) {
return json({ error: 'Stack name must start with a letter or number, and contain only letters, numbers, hyphens, and underscores' }, { status: 400 });
}
data.stackName = trimmedStackName;
}
const oldStackName = existing.stackName;
const updated = await updateGitStack(id, {
stackName: data.stackName,
composePath: data.composePath,
@@ -54,6 +71,11 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
webhookSecret: data.webhookSecret
});
// If stack name changed, update the stack_sources record too
if (data.stackName && data.stackName !== oldStackName) {
await updateStackSourceName(oldStackName, data.stackName, existing.environmentId);
}
// Register or unregister schedule with croner
if (updated.autoUpdate && updated.autoUpdateCron) {
await registerSchedule(id, 'git_stack_sync', updated.environmentId);
@@ -101,6 +123,9 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
// Delete git files first
deleteGitStackFiles(id);
// Delete the stack_sources record to free up the stack name
await deleteStackSource(existing.stackName, existing.environmentId);
// Delete from database
await deleteGitStack(id);
+17 -11
View File
@@ -234,6 +234,10 @@
let batchUpdateContainerIds = $state<string[]>([]);
let batchUpdateContainerNames = $state<Map<string, string>>(new Map());
// Single container update mode (doesn't overwrite batch list)
let singleUpdateContainerId = $state<string | null>(null);
let singleUpdateContainerName = $state<string | null>(null);
// Operation error state
let operationError = $state<{ id: string; message: string } | null>(null);
@@ -459,13 +463,16 @@
}
function updateSingleContainer(containerId: string, containerName: string) {
batchUpdateContainerIds = [containerId];
batchUpdateContainerNames = new Map([[containerId, containerName]]);
// Use single-container mode to avoid overwriting the batch list
singleUpdateContainerId = containerId;
singleUpdateContainerName = containerName;
showBatchUpdateModal = true;
}
function handleBatchUpdateClose() {
showBatchUpdateModal = false;
singleUpdateContainerId = null;
singleUpdateContainerName = null;
updateCheckStatus = 'idle';
}
@@ -481,13 +488,12 @@
}
selectedContainers = new Set();
// Keep blocked containers in the update list - they still have updates available
// Only remove successfully updated containers
const successSet = new Set(results.success);
batchUpdateContainerIds = batchUpdateContainerIds.filter(id => !successSet.has(id));
for (const id of results.success) {
batchUpdateContainerNames.delete(id);
}
// Clear single-update mode
singleUpdateContainerId = null;
singleUpdateContainerName = null;
// Reload pending updates from database to restore highlighting for remaining containers
loadPendingUpdates();
fetchContainers();
}
@@ -2137,8 +2143,8 @@
<BatchUpdateModal
bind:open={showBatchUpdateModal}
containerIds={batchUpdateContainerIds}
containerNames={batchUpdateContainerNames}
containerIds={singleUpdateContainerId ? [singleUpdateContainerId] : batchUpdateContainerIds}
containerNames={singleUpdateContainerId && singleUpdateContainerName ? new Map([[singleUpdateContainerId, singleUpdateContainerName]]) : batchUpdateContainerNames}
{envId}
vulnerabilityCriteria={envHasScanning ? envVulnerabilityCriteria : 'never'}
onClose={handleBatchUpdateClose}
+3 -1
View File
@@ -8,6 +8,7 @@
import * as Card from '$lib/components/ui/card';
import { Loader2, LogIn, Shield, AlertCircle, Network, User, KeyRound, TriangleAlert } from 'lucide-svelte';
import { authStore } from '$lib/stores/auth';
import { environments } from '$lib/stores/environment';
import * as Alert from '$lib/components/ui/alert';
interface AuthProvider {
@@ -96,7 +97,8 @@
return;
}
// Success - redirect
// Success - refresh environments (they were cleared during pre-login fetch) then redirect
await environments.refresh();
goto(redirectUrl);
} catch (e) {
error = 'An unexpected error occurred';
+1 -4
View File
@@ -1724,7 +1724,7 @@
<!-- Clickable ports (dedupe by publicPort for IPv4/IPv6) -->
{#if container.ports.length > 0}
{@const uniquePorts = container.ports.filter((p, i, arr) => p.publicPort && arr.findIndex(x => x.publicPort === p.publicPort) === i)}
{#each uniquePorts.slice(0, 2) as port}
{#each uniquePorts as port}
{@const url = getPortUrl(port.publicPort)}
{#if url}
<a
@@ -1744,9 +1744,6 @@
</span>
{/if}
{/each}
{#if uniquePorts.length > 2}
<span class="text-muted-foreground">+{uniquePorts.length - 2}</span>
{/if}
{/if}
<!-- Network with IP -->
{#if container.networks.length > 0}
+8 -1
View File
@@ -88,6 +88,9 @@
let formError = $state('');
let formSaving = $state(false);
let errors = $state<{ stackName?: string; repository?: string; repoName?: string; repoUrl?: string }>({});
// Stack name validation: must start with alphanumeric, can contain alphanumeric, hyphens, underscores
const STACK_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
let copiedWebhookUrl = $state(false);
let copiedWebhookSecret = $state(false);
@@ -309,9 +312,13 @@
errors = {};
let hasErrors = false;
if (!formStackName.trim()) {
const trimmedStackName = formStackName.trim();
if (!trimmedStackName) {
errors.stackName = 'Stack name is required';
hasErrors = true;
} else if (!STACK_NAME_REGEX.test(trimmedStackName)) {
errors.stackName = 'Stack name must start with a letter or number, and contain only letters, numbers, hyphens, and underscores';
hasErrors = true;
}
if (formRepoMode === 'existing' && !formRepositoryId) {