feat: Able to modify database passwords

This commit is contained in:
Andras Bacsai
2022-04-07 14:29:40 +02:00
parent 4d47eab07c
commit 5bf14f4639
13 changed files with 167 additions and 95 deletions

3
src/app.d.ts vendored
View File

@@ -15,6 +15,9 @@ declare namespace App {
readOnly: boolean; readOnly: boolean;
source: string; source: string;
settings: string; settings: string;
database: Record<string, any>;
versions: string;
privatePort: string;
} }
} }

View File

@@ -56,7 +56,7 @@ export const supportedDatabaseTypesAndVersions = [
name: 'postgresql', name: 'postgresql',
fancyName: 'PostgreSQL', fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql', baseImage: 'bitnami/postgresql',
versions: ['14.2', '13.6', '12.10', '11.15', '10.20'] versions: ['14.2.0', '13.6.0', '12.10.0 ', '11.15.0', '10.20.0']
}, },
{ {
name: 'redis', name: 'redis',

View File

@@ -137,3 +137,37 @@ export async function stopDatabase(database) {
} }
return everStarted; return everStarted;
} }
export async function updatePasswordInDb(database, user, newPassword) {
const {
id,
type,
rootUser,
rootUserPassword,
dbUser,
dbUserPassword,
defaultDatabase,
destinationDockerId,
destinationDocker: { engine }
} = database;
if (destinationDockerId) {
const host = getEngine(engine);
if (type === 'mysql') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
);
} else if (type === 'postgresql') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
);
} else if (type === 'mongodb') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
);
} else if (type === 'redis') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
);
}
}
}

View File

@@ -44,9 +44,7 @@ export async function configureDestinationForDatabase({ id, destinationId }) {
const host = getEngine(engine); const host = getEngine(engine);
if (type && version) { if (type && version) {
const baseImage = getDatabaseImage(type); const baseImage = getDatabaseImage(type);
asyncExecShell( asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`);
`DOCKER_HOST=${host} docker pull ${baseImage}:${version} && echo "FROM ${baseImage}:${version}" | docker build --label coolify.image="true" -t "${baseImage}:${version}" -`
);
} }
} }
} }

View File

@@ -2,6 +2,8 @@
export let database; export let database;
export let privatePort; export let privatePort;
export let settings; export let settings;
export let isRunning;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
@@ -15,15 +17,26 @@
import { browser } from '$app/env'; import { browser } from '$app/env';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
let loading = false; let loading = false;
let publicLoading = false;
let isPublic = database.settings.isPublic || false; let isPublic = database.settings.isPublic || false;
let appendOnly = database.settings.appendOnly; let appendOnly = database.settings.appendOnly;
let databaseDefault = database.defaultDatabase; let databaseDefault;
let databaseDbUser = database.dbUser; let databaseDbUser;
let databaseDbUserPassword = database.dbUserPassword; let databaseDbUserPassword;
generateDbDetails();
function generateDbDetails() {
databaseDefault = database.defaultDatabase;
databaseDbUser = database.dbUser;
databaseDbUserPassword = database.dbUserPassword;
if (database.type === 'mongodb') { if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false'; databaseDefault = '?readPreference=primary&ssl=false';
databaseDbUser = database.rootUser; databaseDbUser = database.rootUser;
@@ -32,7 +45,8 @@
databaseDefault = ''; databaseDefault = '';
databaseDbUser = ''; databaseDbUser = '';
} }
let databaseUrl = generateUrl(); }
$: databaseUrl = generateUrl();
function generateUrl() { function generateUrl() {
return browser return browser
@@ -49,28 +63,46 @@
} }
async function changeSettings(name) { async function changeSettings(name) {
if (publicLoading || !isRunning) return;
publicLoading = true;
let data = {
isPublic,
appendOnly
};
if (name === 'isPublic') { if (name === 'isPublic') {
isPublic = !isPublic; data.isPublic = !isPublic;
} }
if (name === 'appendOnly') { if (name === 'appendOnly') {
appendOnly = !appendOnly; data.appendOnly = !appendOnly;
} }
try { try {
const { publicPort } = await post(`/databases/${id}/settings.json`, { isPublic, appendOnly }); const { publicPort } = await post(`/databases/${id}/settings.json`, {
isPublic: data.isPublic,
appendOnly: data.appendOnly
});
isPublic = data.isPublic;
appendOnly = data.appendOnly;
databaseUrl = generateUrl();
if (isPublic) { if (isPublic) {
database.publicPort = publicPort; database.publicPort = publicPort;
} }
databaseUrl = generateUrl();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
publicLoading = false;
} }
} }
async function handleSubmit() { async function handleSubmit() {
try { try {
await post(`/databases/${id}.json`, { ...database }); loading = true;
return window.location.reload(); await post(`/databases/${id}.json`, { ...database, isRunning });
generateDbDetails();
databaseUrl = generateUrl();
toast.push('Settings saved.');
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
</script> </script>
@@ -142,21 +174,21 @@
readonly readonly
disabled disabled
name="publicPort" name="publicPort"
value={isPublic ? database.publicPort : privatePort} value={publicLoading ? 'Loading...' : isPublic ? database.publicPort : privatePort}
/> />
</div> </div>
</div> </div>
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
{#if database.type === 'mysql'} {#if database.type === 'mysql'}
<MySql bind:database /> <MySql bind:database {isRunning} />
{:else if database.type === 'postgresql'} {:else if database.type === 'postgresql'}
<PostgreSql bind:database /> <PostgreSql bind:database {isRunning} />
{:else if database.type === 'mongodb'} {:else if database.type === 'mongodb'}
<MongoDb {database} /> <MongoDb bind:database {isRunning} />
{:else if database.type === 'redis'} {:else if database.type === 'redis'}
<Redis {database} /> <Redis bind:database {isRunning} />
{:else if database.type === 'couchdb'} {:else if database.type === 'couchdb'}
<CouchDb bind:database /> <CouchDb {database} />
{/if} {/if}
<div class="grid grid-cols-2 items-center px-10 pb-8"> <div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100">Connection String</label> <label for="url" class="text-base font-bold text-stone-100">Connection String</label>
@@ -168,7 +200,7 @@
name="url" name="url"
readonly readonly
disabled disabled
value={databaseUrl} value={publicLoading || loading ? 'Loading...' : databaseUrl}
/> />
</div> </div>
</div> </div>
@@ -179,10 +211,12 @@
<div class="px-10 pb-10"> <div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
loading={publicLoading}
bind:setting={isPublic} bind:setting={isPublic}
on:click={() => changeSettings('isPublic')} on:click={() => changeSettings('isPublic')}
title="Set it public" title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!" description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
disabled={!isRunning}
/> />
</div> </div>
{#if database.type === 'redis'} {#if database.type === 'redis'}

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -21,13 +23,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField <CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField={true} isPasswordField={true}
readonly
disabled
id="rootUserPassword" id="rootUserPassword"
name="rootUserPassword" name="rootUserPassword"
value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -33,14 +35,15 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
readonly disabled={!isRunning}
disabled readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="dbUserPassword" id="dbUserPassword"
name="dbUserPassword" name="dbUserPassword"
value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
@@ -56,13 +59,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField <CopyPasswordField
readonly disabled={!isRunning}
disabled readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="rootUserPassword" id="rootUserPassword"
name="rootUserPassword" name="rootUserPassword"
value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -33,13 +35,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
readonly disabled={!isRunning}
disabled readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="dbUserPassword" id="dbUserPassword"
name="dbUserPassword" name="dbUserPassword"
value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -10,40 +12,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
disabled disabled={!isRunning}
readonly readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="dbUserPassword" id="dbUserPassword"
name="dbUserPassword" name="dbUserPassword"
value={database.dbUserPassword} bind:value={database.dbUserPassword}
/>
</div>
<!-- <div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>
<div class="grid grid-cols-3 items-center">
<label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div> -->
</div>

View File

@@ -15,7 +15,7 @@
const endpoint = `/databases/${params.id}.json`; const endpoint = `/databases/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
const { database, state, versions, privatePort, settings } = await res.json(); const { database, isRunning, versions, privatePort, settings } = await res.json();
if (!database || Object.entries(database).length === 0) { if (!database || Object.entries(database).length === 0) {
return { return {
status: 302, status: 302,
@@ -35,13 +35,13 @@
return { return {
props: { props: {
database, database,
state, isRunning,
versions, versions,
privatePort privatePort
}, },
stuff: { stuff: {
database, database,
state, isRunning,
versions, versions,
privatePort, privatePort,
settings settings
@@ -65,7 +65,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
export let database; export let database;
export let state; export let isRunning;
let loading = false; let loading = false;
async function deleteDatabase() { async function deleteDatabase() {
@@ -91,8 +91,6 @@
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
} }
@@ -103,8 +101,6 @@
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
</script> </script>
@@ -114,7 +110,7 @@
<Loading fullscreen cover /> <Loading fullscreen cover />
{:else} {:else}
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase} {#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if state === 'running'} {#if isRunning}
<button <button
on:click={stopDatabase} on:click={stopDatabase}
title="Stop database" title="Stop database"
@@ -140,7 +136,7 @@
<rect x="14" y="5" width="4" height="14" rx="1" /> <rect x="14" y="5" width="4" height="14" rx="1" />
</svg> </svg>
</button> </button>
{:else if state === 'not started'} {:else}
<button <button
on:click={startDatabase} on:click={startDatabase}
title="Start database" title="Start database"

View File

@@ -1,6 +1,11 @@
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { generateDatabaseConfiguration, getVersions, ErrorHandler } from '$lib/database'; import {
generateDatabaseConfiguration,
getVersions,
ErrorHandler,
updatePasswordInDb
} from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
@@ -12,7 +17,7 @@ export const get: RequestHandler = async (event) => {
const database = await db.getDatabase({ id, teamId }); const database = await db.getDatabase({ id, teamId });
const { destinationDockerId, destinationDocker } = database; const { destinationDockerId, destinationDocker } = database;
let state = 'not started'; let isRunning = false;
if (destinationDockerId) { if (destinationDockerId) {
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
@@ -22,7 +27,7 @@ export const get: RequestHandler = async (event) => {
); );
if (JSON.parse(stdout).Running) { if (JSON.parse(stdout).Running) {
state = 'running'; isRunning = true;
} }
} catch (error) { } catch (error) {
// //
@@ -34,7 +39,7 @@ export const get: RequestHandler = async (event) => {
body: { body: {
privatePort: configuration?.privatePort, privatePort: configuration?.privatePort,
database, database,
state, isRunning,
versions: getVersions(database.type), versions: getVersions(database.type),
settings settings
} }
@@ -48,10 +53,26 @@ export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const { name, defaultDatabase, dbUser, dbUserPassword, rootUser, rootUserPassword, version } = const {
await event.request.json(); name,
defaultDatabase,
dbUser,
dbUserPassword,
rootUser,
rootUserPassword,
version,
isRunning
} = await event.request.json();
try { try {
const database = await db.getDatabase({ id, teamId });
if (isRunning) {
if (database.dbUserPassword !== dbUserPassword) {
await updatePasswordInDb(database, dbUser, dbUserPassword);
} else if (database.rootUserPassword !== rootUserPassword) {
await updatePasswordInDb(database, rootUser, rootUserPassword);
}
}
await db.updateDatabase({ await db.updateDatabase({
id, id,
name, name,

View File

@@ -8,7 +8,8 @@
database: stuff.database, database: stuff.database,
versions: stuff.versions, versions: stuff.versions,
privatePort: stuff.privatePort, privatePort: stuff.privatePort,
settings: stuff.settings settings: stuff.settings,
isRunning: stuff.isRunning
} }
}; };
} }
@@ -35,6 +36,7 @@
export let database; export let database;
export let settings; export let settings;
export let privatePort; export let privatePort;
export let isRunning;
</script> </script>
<div class="flex items-center space-x-2 p-6 text-2xl font-bold"> <div class="flex items-center space-x-2 p-6 text-2xl font-bold">
@@ -47,4 +49,4 @@
<DatabaseLinks {database} /> <DatabaseLinks {database} />
</div> </div>
<Databases bind:database {privatePort} {settings} /> <Databases bind:database {privatePort} {settings} {isRunning} />

View File

@@ -12,8 +12,8 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
const { id } = $page.params; const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock'; let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
let loading = false; let loading = false;
let loadingProxy = false;
let restarting = false; let restarting = false;
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
@@ -25,12 +25,6 @@
loading = false; loading = false;
} }
} }
// async function scanApps() {
// scannedApps = [];
// const data = await fetch(`/destinations/${id}/scan.json`);
// const { containers } = await data.json();
// scannedApps = containers;
// }
onMount(async () => { onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) { if (state === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
@@ -71,6 +65,7 @@
} }
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try { try {
loadingProxy = true;
await post(`/destinations/${id}/settings.json`, { await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine engine: destination.engine
@@ -82,6 +77,8 @@
} }
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loadingProxy = false;
} }
} }
} }
@@ -187,6 +184,7 @@
{#if $session.teamId === '0'} {#if $session.teamId === '0'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
loading={loadingProxy}
disabled={cannotDisable} disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}