This commit is contained in:
jarek committed 2026-01-28 07:34:12 +01:00
1 parent a88d3d5788
commit 8f3a7eb435
96 files changed
+5235 -1870

No files matched your search

+6 -1
View File
@@ -6,6 +6,7 @@ import {
type GitAuthType
} from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { auditGitCredential } from '$lib/server/audit';
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
@@ -33,7 +34,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
}
};
export const POST: RequestHandler = async ({ request, cookies }) => {
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('git', 'create')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -68,6 +70,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
sshPassphrase: data.sshPassphrase
});
// Audit log
await auditGitCredential(event, 'create', credential.id, credential.name);
return json({
id: credential.id,
name: credential.name,
+26 -4
View File
@@ -7,6 +7,8 @@ import {
type GitAuthType
} from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { auditGitCredential } from '$lib/server/audit';
import { computeAuditDiff } from '$lib/utils/diff';
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
@@ -42,7 +44,8 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
}
};
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('git', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -78,6 +81,15 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
return json({ error: 'Failed to update credential' }, { status: 500 });
}
// Compute diff for audit (only non-sensitive fields)
const diff = computeAuditDiff(
{ name: existing.name, authType: existing.authType, username: existing.username },
{ name: credential.name, authType: credential.authType, username: credential.username }
);
// Audit log
await auditGitCredential(event, 'update', credential.id, credential.name, diff);
return json({
id: credential.id,
name: credential.name,
@@ -97,7 +109,8 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
}
};
export const DELETE: RequestHandler = async ({ params, cookies }) => {
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('git', 'delete')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -109,11 +122,20 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
return json({ error: 'Invalid credential ID' }, { status: 400 });
}
const deleted = await deleteGitCredential(id);
if (!deleted) {
// Get credential name before deletion for audit log
const credential = await getGitCredential(id);
if (!credential) {
return json({ error: 'Credential not found' }, { status: 404 });
}
const deleted = await deleteGitCredential(id);
if (!deleted) {
return json({ error: 'Failed to delete credential' }, { status: 500 });
}
// Audit log
await auditGitCredential(event, 'delete', id, credential.name);
return json({ success: true });
} catch (error) {
console.error('Failed to delete git credential:', error);
+92
View File
@@ -0,0 +1,92 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getGitRepository, getGitCredential } from '$lib/server/db';
import { previewRepoEnvFiles } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
/**
* POST /api/git/preview-env
* Clone a git repository to a temp directory and read env files for preview.
* Used when creating a new git stack to populate the env editor.
*
* Body: {
* repositoryId?: number, // Existing repository
* url?: string, // OR new repo URL
* branch?: string, // Branch (default: main)
* credentialId?: number, // Credential for auth
* composePath: string, // Path to compose file
* envFilePath?: string // Optional additional env file
* }
*
* Returns: {
* vars: Record<string, string>, // Merged env variables
* sources: { // Which file each var came from
* [key: string]: '.env' | 'envFile'
* },
* error?: string
* }
*/
export const POST: RequestHandler = async ({ request, cookies }) => {
const auth = await authorize(cookies);
// Basic permission check - must be able to create stacks
if (auth.authEnabled && !auth.isAuthenticated) {
return json({ error: 'Authentication required' }, { status: 401 });
}
try {
const data = await request.json();
if (!data.composePath || typeof data.composePath !== 'string') {
return json({ error: 'Compose path is required' }, { status: 400 });
}
let repoUrl: string;
let branch: string = 'main';
let credentialId: number | null = null;
if (data.repositoryId) {
// Use existing repository
const repo = await getGitRepository(data.repositoryId);
if (!repo) {
return json({ error: 'Repository not found' }, { status: 404 });
}
repoUrl = repo.url;
branch = repo.branch;
credentialId = repo.credentialId;
} else if (data.url) {
// New repository details
repoUrl = data.url;
branch = data.branch || 'main';
credentialId = data.credentialId || null;
} else {
return json({ error: 'Either repositoryId or url is required' }, { status: 400 });
}
// Get credential if specified
let credential = null;
if (credentialId) {
credential = await getGitCredential(credentialId);
}
const result = await previewRepoEnvFiles({
repoUrl,
branch,
credential,
composePath: data.composePath,
envFilePath: data.envFilePath || null
});
if (result.error) {
return json({ vars: {}, sources: {}, error: result.error }, { status: 400 });
}
return json({
vars: result.vars,
sources: result.sources
});
} catch (error: any) {
console.error('Failed to preview env files:', error);
return json({ error: error.message || 'Failed to preview env files' }, { status: 500 });
}
};
+6 -1
View File
@@ -6,6 +6,7 @@ import {
getGitCredentials
} from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { auditGitRepository } from '$lib/server/audit';
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
@@ -24,7 +25,8 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
}
};
export const POST: RequestHandler = async ({ request, cookies }) => {
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('git', 'create')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -59,6 +61,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
credentialId: data.credentialId || null
});
// Audit log
await auditGitRepository(event, 'create', repository.id, repository.name);
return json(repository);
} catch (error: any) {
console.error('Failed to create git repository:', error);
@@ -8,6 +8,8 @@ import {
} from '$lib/server/db';
import { deleteRepositoryFiles } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
import { auditGitRepository } from '$lib/server/audit';
import { computeAuditDiff } from '$lib/utils/diff';
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
@@ -33,7 +35,8 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
}
};
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('git', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -74,6 +77,12 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
return json({ error: 'Failed to update repository' }, { status: 500 });
}
// Compute diff for audit
const diff = computeAuditDiff(existing, repository);
// Audit log
await auditGitRepository(event, 'update', repository.id, repository.name, diff);
return json(repository);
} catch (error: any) {
console.error('Failed to update git repository:', error);
@@ -84,7 +93,8 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
}
};
export const DELETE: RequestHandler = async ({ params, cookies }) => {
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('git', 'delete')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -96,14 +106,23 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
return json({ error: 'Invalid repository ID' }, { status: 400 });
}
// Get repository name before deletion for audit log
const repository = await getGitRepository(id);
if (!repository) {
return json({ error: 'Repository not found' }, { status: 404 });
}
// Delete repository files first
deleteRepositoryFiles(id);
const deleted = await deleteGitRepository(id);
if (!deleted) {
return json({ error: 'Repository not found' }, { status: 404 });
return json({ error: 'Failed to delete repository' }, { status: 500 });
}
// Audit log
await auditGitRepository(event, 'delete', id, repository.name);
return json({ success: true });
} catch (error) {
console.error('Failed to delete git repository:', error);
+22 -2
View File
@@ -6,12 +6,14 @@ import {
getGitCredentials,
getGitRepository,
createGitRepository,
upsertStackSource
upsertStackSource,
setStackEnvVars
} from '$lib/server/db';
import { deployGitStack } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
import { registerSchedule } from '$lib/server/scheduler';
import { secureRandomBytes } from '$lib/server/crypto-fallback';
import { auditGitStack } from '$lib/server/audit';
// Stack name validation: must start with alphanumeric, can contain alphanumeric, hyphens, underscores
const STACK_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
@@ -37,7 +39,8 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
}
};
export const POST: RequestHandler = async ({ request, cookies }) => {
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
try {
@@ -132,9 +135,26 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
await registerSchedule(gitStack.id, 'git_stack_sync', gitStack.environmentId);
}
// Audit log
await auditGitStack(event, 'create', gitStack.id, gitStack.stackName, gitStack.environmentId);
// Save environment variable overrides before deploying
if (data.envVars && Array.isArray(data.envVars) && data.envVars.length > 0) {
await setStackEnvVars(
trimmedStackName,
data.environmentId || null,
data.envVars.filter((v: any) => v.key?.trim()).map((v: any) => ({
key: v.key.trim(),
value: v.value ?? '',
isSecret: v.isSecret ?? false
}))
);
}
// If deployNow is set, deploy immediately
if (data.deployNow) {
const deployResult = await deployGitStack(gitStack.id);
await auditGitStack(event, 'deploy', gitStack.id, gitStack.stackName, gitStack.environmentId);
return json({
...gitStack,
deployResult: deployResult
+35 -4
View File
@@ -1,9 +1,11 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getGitStack, updateGitStack, deleteGitStack, deleteStackSource, updateStackSourceName, updateStackEnvVarsName } from '$lib/server/db';
import { getGitStack, updateGitStack, deleteGitStack, deleteStackSource, updateStackSourceName, updateStackEnvVarsName, setStackEnvVars } from '$lib/server/db';
import { deleteGitStackFiles, deployGitStack } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
import { registerSchedule, unregisterSchedule } from '$lib/server/scheduler';
import { auditGitStack } from '$lib/server/audit';
import { computeAuditDiff } from '$lib/utils/diff';
// Stack name validation: must start with alphanumeric, can contain alphanumeric, hyphens, underscores
const STACK_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
@@ -30,7 +32,8 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
}
};
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
try {
@@ -84,9 +87,33 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
unregisterSchedule(id, 'git_stack_sync');
}
// Compute diff for audit (exclude sensitive fields)
const diff = computeAuditDiff(existing, updated, {
excludeFields: ['webhookSecret', 'createdAt', 'updatedAt', 'lastSync', 'lastCommit', 'syncStatus', 'syncError']
});
// Audit log
await auditGitStack(event, 'update', updated.id, updated.stackName, updated.environmentId, diff);
// Save environment variable overrides before deploying
if (data.envVars && Array.isArray(data.envVars)) {
const stackName = data.stackName || existing.stackName;
const envId = updated.environmentId ?? null;
await setStackEnvVars(
stackName,
envId,
data.envVars.filter((v: any) => v.key?.trim()).map((v: any) => ({
key: v.key.trim(),
value: v.value ?? '',
isSecret: v.isSecret ?? false
}))
);
}
// If deployNow is set, deploy after saving
if (data.deployNow) {
const deployResult = await deployGitStack(id);
await auditGitStack(event, 'deploy', updated.id, updated.stackName, updated.environmentId);
return json({
...updated,
deployResult
@@ -103,7 +130,8 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
}
};
export const DELETE: RequestHandler = async ({ params, cookies }) => {
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
try {
@@ -122,7 +150,7 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
unregisterSchedule(id, 'git_stack_sync');
// Delete git files first
deleteGitStackFiles(id);
await deleteGitStackFiles(id, existing.stackName, existing.environmentId);
// Delete the stack_sources record to free up the stack name
await deleteStackSource(existing.stackName, existing.environmentId);
@@ -130,6 +158,9 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
// Delete from database
await deleteGitStack(id);
// Audit log
await auditGitStack(event, 'delete', id, existing.stackName, existing.environmentId);
return json({ success: true });
} catch (error) {
console.error('Failed to delete git stack:', error);
@@ -3,8 +3,10 @@ import type { RequestHandler } from './$types';
import { getGitStack } from '$lib/server/db';
import { deployGitStack } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
import { auditGitStack } from '$lib/server/audit';
export const POST: RequestHandler = async ({ params, cookies }) => {
export const POST: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
try {
@@ -20,6 +22,10 @@ export const POST: RequestHandler = async ({ params, cookies }) => {
}
const result = await deployGitStack(id);
// Audit log
await auditGitStack(event, 'deploy', id, gitStack.stackName, gitStack.environmentId);
return json(result);
} catch (error) {
console.error('Failed to deploy git stack:', error);