1.0.8
This commit is contained in:
1 parent
027aee434c
commit
62636426bf
15 files changed
+317
-65
No files matched your search
@@ -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());
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user