1.0.5
This commit is contained in:
1 parent
5475112806
commit
242f8df49d
35 files changed
+1892
-1987
No files matched your search
@@ -86,6 +86,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
tlsCa: data.tlsCa,
|
||||
tlsCert: data.tlsCert,
|
||||
tlsKey: data.tlsKey,
|
||||
tlsSkipVerify: data.tlsSkipVerify || false,
|
||||
icon: data.icon || 'globe',
|
||||
socketPath: data.socketPath || '/var/run/docker.sock',
|
||||
collectActivity: data.collectActivity !== false,
|
||||
|
||||
@@ -65,6 +65,7 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
|
||||
tlsCa: data.tlsCa,
|
||||
tlsCert: data.tlsCert,
|
||||
tlsKey: data.tlsKey,
|
||||
tlsSkipVerify: data.tlsSkipVerify,
|
||||
icon: data.icon,
|
||||
socketPath: data.socketPath,
|
||||
collectActivity: data.collectActivity,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { listComposeStacks, deployStack, saveStackComposeFile } from '$lib/server/stacks';
|
||||
import { listComposeStacks, deployStack, saveStackComposeFile, saveStackEnvVars } from '$lib/server/stacks';
|
||||
import { EnvironmentNotFoundError } from '$lib/server/docker';
|
||||
import { upsertStackSource, getStackSources } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
@@ -78,7 +78,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, compose, start } = body;
|
||||
const { name, compose, start, envVars } = body;
|
||||
|
||||
if (!name || typeof name !== 'string') {
|
||||
return json({ error: 'Stack name is required' }, { status: 400 });
|
||||
@@ -95,6 +95,11 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
|
||||
return json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
// Save environment variables if provided (to both DB and .env file)
|
||||
if (envVars && Array.isArray(envVars) && envVars.length > 0) {
|
||||
await saveStackEnvVars(name, envVars, envIdNum);
|
||||
}
|
||||
|
||||
// Record the stack as internally created
|
||||
await upsertStackSource({
|
||||
stackName: name,
|
||||
@@ -105,6 +110,14 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
|
||||
return json({ success: true, started: false });
|
||||
}
|
||||
|
||||
// Save environment variables BEFORE deploying so they're available during start
|
||||
if (envVars && Array.isArray(envVars) && envVars.length > 0) {
|
||||
// First ensure the stack directory exists by saving compose file
|
||||
await saveStackComposeFile(name, compose, true);
|
||||
// Save to both DB and .env file
|
||||
await saveStackEnvVars(name, envVars, envIdNum);
|
||||
}
|
||||
|
||||
// Deploy and start the stack
|
||||
const result = await deployStack({
|
||||
name,
|
||||
|
||||
+156
-8
@@ -1,11 +1,96 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getStackEnvVars, setStackEnvVars } from '$lib/server/db';
|
||||
import { getStacksDir } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
/**
|
||||
* Parse a .env file content into key-value pairs
|
||||
*/
|
||||
function parseEnvFile(content: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
// Skip empty lines and comments
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eqIndex = trimmed.indexOf('=');
|
||||
if (eqIndex > 0) {
|
||||
const key = trimmed.substring(0, eqIndex).trim();
|
||||
let value = trimmed.substring(eqIndex + 1);
|
||||
// Remove surrounding quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge new variables into existing .env file content.
|
||||
* - Keeps comments and formatting
|
||||
* - Updates values for existing keys
|
||||
* - REMOVES keys that are not in newVars (user deleted them)
|
||||
* - Appends new keys at the end
|
||||
*/
|
||||
function mergeEnvFileContent(
|
||||
existingContent: string,
|
||||
newVars: { key: string; value: string }[]
|
||||
): string {
|
||||
const newVarsMap = new Map(newVars.map(v => [v.key, v.value]));
|
||||
const handledKeys = new Set<string>();
|
||||
const lines = existingContent.split('\n');
|
||||
const resultLines: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Keep comments and blank lines as-is
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
resultLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a variable line
|
||||
const eqIndex = trimmed.indexOf('=');
|
||||
if (eqIndex > 0) {
|
||||
const key = trimmed.substring(0, eqIndex).trim();
|
||||
|
||||
if (newVarsMap.has(key)) {
|
||||
// Update existing variable with new value from UI
|
||||
resultLines.push(`${key}=${newVarsMap.get(key)}`);
|
||||
handledKeys.add(key);
|
||||
}
|
||||
// If key not in newVarsMap, it was deleted - skip it (don't add to resultLines)
|
||||
} else {
|
||||
// Not a valid variable line, keep as-is
|
||||
resultLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Append any new variables that weren't in the original file
|
||||
for (const v of newVars) {
|
||||
if (!handledKeys.has(v.key)) {
|
||||
resultLines.push(`${v.key}=${v.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure file ends with newline
|
||||
let result = resultLines.join('\n');
|
||||
if (!result.endsWith('\n')) {
|
||||
result += '\n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/stacks/[name]/env?env=X
|
||||
* Get all environment variables for a stack.
|
||||
* Merges variables from database with .env file (file values shown if different).
|
||||
* Secrets are masked with '***' in the response.
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
@@ -25,15 +110,52 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
try {
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
const variables = await getStackEnvVars(stackName, envIdNum, true);
|
||||
|
||||
return json({
|
||||
variables: variables.map(v => ({
|
||||
key: v.key,
|
||||
value: v.value,
|
||||
isSecret: v.isSecret
|
||||
}))
|
||||
});
|
||||
// Get variables from database
|
||||
const dbVariables = await getStackEnvVars(stackName, envIdNum, true);
|
||||
const dbByKey = new Map(dbVariables.map(v => [v.key, v]));
|
||||
|
||||
// Try to read .env file from stack directory
|
||||
const stacksDir = getStacksDir();
|
||||
const envFilePath = join(stacksDir, stackName, '.env');
|
||||
let fileVars: Record<string, string> = {};
|
||||
|
||||
if (existsSync(envFilePath)) {
|
||||
try {
|
||||
const content = await Bun.file(envFilePath).text();
|
||||
fileVars = parseEnvFile(content);
|
||||
} catch (e) {
|
||||
// Ignore file read errors
|
||||
}
|
||||
}
|
||||
|
||||
// Merge: start with DB variables, add any new keys from file
|
||||
const mergedKeys = new Set([...dbByKey.keys(), ...Object.keys(fileVars)]);
|
||||
const variables: { key: string; value: string; isSecret: boolean }[] = [];
|
||||
|
||||
for (const key of mergedKeys) {
|
||||
const dbVar = dbByKey.get(key);
|
||||
const fileValue = fileVars[key];
|
||||
|
||||
if (dbVar) {
|
||||
// Variable exists in DB
|
||||
if (dbVar.isSecret) {
|
||||
// Keep secret masked
|
||||
variables.push({ key, value: dbVar.value, isSecret: true });
|
||||
} else if (fileValue !== undefined && fileValue !== dbVar.value) {
|
||||
// File has different value - use file value (user may have edited it)
|
||||
variables.push({ key, value: fileValue, isSecret: false });
|
||||
} else {
|
||||
// Use DB value
|
||||
variables.push({ key, value: dbVar.value, isSecret: false });
|
||||
}
|
||||
} else if (fileValue !== undefined) {
|
||||
// Variable only in file - add it as non-secret
|
||||
variables.push({ key, value: fileValue, isSecret: false });
|
||||
}
|
||||
}
|
||||
|
||||
return json({ variables });
|
||||
} catch (error) {
|
||||
console.error('Error getting stack env vars:', error);
|
||||
return json({ error: 'Failed to get environment variables' }, { status: 500 });
|
||||
@@ -114,6 +236,32 @@ export const PUT: RequestHandler = async ({ params, url, cookies, request }) =>
|
||||
|
||||
await setStackEnvVars(stackName, envIdNum, variablesToSave);
|
||||
|
||||
// Also write the .env file to the stack directory
|
||||
// This allows users to see/edit variables outside of Dockhand
|
||||
const stacksDir = getStacksDir();
|
||||
const stackDir = join(stacksDir, stackName);
|
||||
const envFilePath = join(stackDir, '.env');
|
||||
|
||||
// Only write if stack directory exists
|
||||
if (existsSync(stackDir)) {
|
||||
// Read existing file to preserve comments and formatting
|
||||
let existingContent = '';
|
||||
if (existsSync(envFilePath)) {
|
||||
try {
|
||||
existingContent = await Bun.file(envFilePath).text();
|
||||
} catch {
|
||||
// File read failed, start fresh
|
||||
}
|
||||
}
|
||||
|
||||
// Merge UI vars with existing file (preserves comments, keeps file vars)
|
||||
const envContent = mergeEnvFileContent(
|
||||
existingContent,
|
||||
variablesToSave.map((v: { key: string; value: string }) => ({ key: v.key, value: v.value }))
|
||||
);
|
||||
await Bun.write(envFilePath, envContent);
|
||||
}
|
||||
|
||||
return json({ success: true, count: variablesToSave.length });
|
||||
} catch (error) {
|
||||
console.error('Error setting stack env vars:', error);
|
||||
|
||||
@@ -128,9 +128,15 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
}, { status: 201 });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to create user:', error);
|
||||
if (error.message?.includes('UNIQUE constraint failed')) {
|
||||
console.error('Error details:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
name: error.name,
|
||||
stack: error.stack
|
||||
});
|
||||
if (error.message?.includes('UNIQUE constraint failed') || error.code === '23505') {
|
||||
return json({ error: 'Username already exists' }, { status: 409 });
|
||||
}
|
||||
return json({ error: 'Failed to create user' }, { status: 500 });
|
||||
return json({ error: 'Failed to create user', details: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { isEnterprise } from '$lib/server/license';
|
||||
import {
|
||||
validateSession,
|
||||
checkPermission,
|
||||
generateMfaSetup,
|
||||
verifyAndEnableMfa,
|
||||
disableMfa
|
||||
@@ -11,11 +9,6 @@ import {
|
||||
|
||||
// POST /api/users/[id]/mfa - Setup MFA (generate QR code)
|
||||
export const POST: RequestHandler = async ({ params, request, cookies }) => {
|
||||
// Check enterprise license
|
||||
if (!(await isEnterprise())) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const currentUser = await validateSession(cookies);
|
||||
|
||||
if (!params.id) {
|
||||
@@ -38,12 +31,16 @@ export const POST: RequestHandler = async ({ params, request, cookies }) => {
|
||||
return json({ error: 'MFA token is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const success = await verifyAndEnableMfa(userId, body.token);
|
||||
if (!success) {
|
||||
const result = await verifyAndEnableMfa(userId, body.token);
|
||||
if (!result.success) {
|
||||
return json({ error: 'Invalid MFA code' }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ success: true, message: 'MFA enabled successfully' });
|
||||
return json({
|
||||
success: true,
|
||||
message: 'MFA enabled successfully',
|
||||
backupCodes: result.backupCodes
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new MFA setup
|
||||
@@ -64,11 +61,6 @@ export const POST: RequestHandler = async ({ params, request, cookies }) => {
|
||||
|
||||
// DELETE /api/users/[id]/mfa - Disable MFA
|
||||
export const DELETE: RequestHandler = async ({ params, cookies }) => {
|
||||
// Check enterprise license
|
||||
if (!(await isEnterprise())) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const currentUser = await validateSession(cookies);
|
||||
|
||||
if (!params.id) {
|
||||
|
||||
Reference in new issue
Block a user