feat: remote docker engine init

This commit is contained in:
Andras Bacsai 2022-07-18 14:02:53 +00:00
parent 0a8fd0516d
commit 537209d3fb
20 changed files with 809 additions and 429 deletions

View File

@ -16,7 +16,7 @@
"dependencies": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.1.0",
"@fastify/cookie": "7.1.0",
"@fastify/cookie": "7.2.0",
"@fastify/cors": "8.0.0",
"@fastify/env": "4.0.0",
"@fastify/jwt": "6.3.1",
@ -43,16 +43,17 @@
"node-forge": "1.3.1",
"node-os-utils": "1.3.7",
"p-queue": "7.2.0",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1"
},
"devDependencies": {
"@types/node": "18.0.4",
"@types/node": "18.0.6",
"@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"esbuild": "0.14.49",
"eslint": "8.19.0",
"eslint": "8.20.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19",

View File

@ -0,0 +1,21 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_DestinationDocker" (
"id" TEXT NOT NULL PRIMARY KEY,
"network" TEXT NOT NULL,
"name" TEXT NOT NULL,
"engine" TEXT,
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
"remoteIpAddress" TEXT,
"remoteUser" TEXT,
"remotePort" INTEGER,
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt" FROM "DestinationDocker";
DROP TABLE "DestinationDocker";
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,36 @@
-- CreateTable
CREATE TABLE "SshKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"privateKey" TEXT NOT NULL,
"destinationDockerId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "SshKey_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_DestinationDocker" (
"id" TEXT NOT NULL PRIMARY KEY,
"network" TEXT NOT NULL,
"name" TEXT NOT NULL,
"engine" TEXT,
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
"remoteIpAddress" TEXT,
"remoteUser" TEXT,
"remotePort" INTEGER,
"remoteVerified" BOOLEAN NOT NULL DEFAULT false,
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt" FROM "DestinationDocker";
DROP TABLE "DestinationDocker";
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
-- CreateIndex
CREATE UNIQUE INDEX "SshKey_destinationDockerId_key" ON "SshKey"("destinationDockerId");

View File

@ -200,8 +200,12 @@ model DestinationDocker {
id String @id @default(cuid())
network String @unique
name String
engine String
engine String?
remoteEngine Boolean @default(false)
remoteIpAddress String?
remoteUser String?
remotePort Int?
remoteVerified Boolean @default(false)
isCoolifyProxyUsed Boolean? @default(false)
teams Team[]
application Application[]
@ -209,6 +213,17 @@ model DestinationDocker {
updatedAt DateTime @updatedAt
database Database[]
service Service[]
sshKey SshKey?
}
model SshKey {
id String @id @default(cuid())
name String
privateKey String
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
destinationDockerId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model GitSource {

View File

@ -1,6 +1,8 @@
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { asyncExecShell, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import sshConfig from 'ssh-config'
import fs from 'fs/promises'
import { asyncExecShell, decrypt, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine } from '../../../../lib/docker';
import type { OnlyId } from '../../../../types';
@ -44,7 +46,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
const { id } = request.params
const teamId = request.user?.teamId;
const destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { sshKey: true }
});
if (!destination && id !== 'new') {
throw { status: 404, message: `Destination not found.` };
@ -80,39 +83,51 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) {
try {
const { id } = request.params
let { name, network, engine, isCoolifyProxyUsed } = request.body
let { name, network, engine, isCoolifyProxyUsed, ipAddress, user, port, sshPrivateKey } = request.body
const teamId = request.user.teamId;
if (id === 'new') {
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
if (found.length === 0) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
});
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
const destination = destinations.find((destination) => destination.network === network);
if (engine) {
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
if (found.length === 0) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
});
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
const destination = destinations.find((destination) => destination.network === network);
if (destinations.length > 0) {
const proxyConfigured = destinations.find(
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
if (destinations.length > 0) {
const proxyConfigured = destinations.find(
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
if (isCoolifyProxyUsed) {
const settings = await prisma.setting.findFirst();
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine);
if (isCoolifyProxyUsed) {
const settings = await prisma.setting.findFirst();
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine);
}
}
return reply.code(201).send({ id: destination.id });
}
return reply.code(201).send({ id: destination.id });
if (ipAddress) {
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress: ipAddress, remoteUser: user, remotePort: port }
});
return reply.code(201).send()
}
throw {
message: `Cannot save Docker Engine.`
};
} else {
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
return reply.code(201).send();
@ -120,6 +135,8 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
} catch ({ status, message }) {
return errorHandler({ status, message })
} finally {
await fs.rm('./id_rsa')
}
}
export async function deleteDestination(request: FastifyRequest<OnlyId>) {
@ -195,3 +212,45 @@ export async function restartProxy(request: FastifyRequest<Proxy>) {
return errorHandler({ status, message })
}
}
export async function assignSSHKey(request: FastifyRequest) {
try {
const { id: sshKeyId } = request.body;
const { id } = request.params;
console.log({ id, sshKeyId })
await prisma.destinationDocker.update({ where: { id }, data: { sshKey: { connect: { id: sshKeyId } } } })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser, network } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
await fs.writeFile('./id_rsa', decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
const host = `ssh://${remoteUser}@${remoteIpAddress}`
const config = sshConfig.parse('')
const found = config.find({ Host: remoteIpAddress })
if (!found) {
config.append({
Host: remoteIpAddress,
Port: remotePort.toString(),
User: remoteUser,
IdentityFile: '/workspace/coolify/apps/api/id_rsa',
StrictHostKeyChecking: 'no'
})
}
await fs.writeFile('/home/gitpod/.ssh/config', sshConfig.stringify(config))
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
console.log({ stdout })
if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDestination, deleteDestination, getDestination, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy } from './handlers';
import { assignSSHKey, checkDestination, deleteDestination, getDestination, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers';
import type { OnlyId } from '../../../../types';
import type { CheckDestination, NewDestination, Proxy, SaveDestinationSettings } from './types';
@ -15,10 +15,14 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteDestination(request));
fastify.post<SaveDestinationSettings>('/:id/settings', async (request, reply) => await saveDestinationSettings(request));
fastify.post<Proxy>('/:id/start', async (request, reply) => await startProxy(request));
fastify.post<Proxy>('/:id/stop', async (request, reply) => await stopProxy(request));
fastify.post<Proxy>('/:id/restart', async (request, reply) => await restartProxy(request));
fastify.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request));
fastify.post<Proxy>('/:id/start', async (request,) => await startProxy(request));
fastify.post<Proxy>('/:id/stop', async (request) => await stopProxy(request));
fastify.post<Proxy>('/:id/restart', async (request) => await restartProxy(request));
fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request));
fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply));
};
export default root;

View File

@ -1,15 +1,23 @@
import { promises as dns } from 'dns';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) {
try {
const settings = await listSettings();
const sshKeys = await prisma.sshKey.findMany()
const unencryptedKeys = []
if (sshKeys.length > 0) {
for (const key of sshKeys) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
}
}
return {
settings
settings,
sshKeys: unencryptedKeys
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -83,4 +91,30 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: FastifyReply) {
try {
const { privateKey, name } = request.body;
const found = await prisma.sshKey.findMany({ where: { name } })
if (found.length > 0) {
throw {
message: "Name already used. Choose another one please."
}
}
const encryptedSSHKey = encrypt(privateKey)
await prisma.sshKey.create({ data: { name, privateKey: encryptedSSHKey } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.sshKey.delete({ where: { id } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, listAllSettings, saveSettings } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@ -13,6 +13,9 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
};
export default root;

View File

@ -28,4 +28,15 @@ export interface CheckDNS {
Params: {
domain: string,
}
}
export interface SaveSSHKey {
Body: {
privateKey: string,
name: string
}
}
export interface DeleteSSHKey {
Body: {
id: string
}
}

View File

@ -15,13 +15,13 @@
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@playwright/test": "1.23.3",
"@sveltejs/kit": "1.0.0-next.375",
"@playwright/test": "1.23.4",
"@sveltejs/kit": "1.0.0-next.377",
"@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"autoprefixer": "10.4.7",
"eslint": "8.19.0",
"eslint": "8.20.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0",
"postcss": "8.4.14",
@ -34,11 +34,11 @@
"tailwindcss-scrollbar": "0.1.0",
"tslib": "2.4.0",
"typescript": "4.7.4",
"vite": "^3.0.0"
"vite": "3.0.1"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "1.0.0-next.36",
"@sveltejs/adapter-static": "1.0.0-next.37",
"@zerodevx/svelte-toast": "0.7.2",
"cuid": "2.1.8",
"js-cookie": "3.0.1",

View File

@ -142,9 +142,6 @@
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
@ -168,10 +165,6 @@
value={destination.engine}
/>
</div>
<!-- <div class="flex items-center">
<label for="remoteEngine">Remote Engine?</label>
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField

View File

@ -44,7 +44,7 @@
<button class="w-32" on:click={() => setPredefined('localDocker')}
>{$t('sources.local_docker')}</button
>
<!-- <button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button> -->
<button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button>
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
</div>
</div>

View File

@ -11,13 +11,19 @@
let loading = false;
async function handleSubmit() {
if (loading) return;
try {
const { id } = await post('/new/destination/docker', {
loading = true;
await post(`/destinations/check`, { network: payload.network });
const { id } = await post(`/destinations/new`, {
...payload
});
return await goto(`/destinations/${id}`);
await goto(`/destinations/${id}`);
window.location.reload();
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
@ -64,20 +70,6 @@
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input required name="port" placeholder="{$t('forms.eg')}: 22" bind:value={payload.port} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshPrivateKey" class="text-base font-bold text-stone-100"
>{$t('forms.ssh_private_key')}</label
>
<textarea
rows="10"
class="resize-none"
required
name="sshPrivateKey"
placeholder="{$t('forms.eg')}: -----BEGIN...."
bind:value={payload.sshPrivateKey}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input

View File

@ -1,7 +1,7 @@
<script lang="ts">
export let destination: any;
export let settings: any
export let state: any
export let settings: any;
export let state: any;
import { toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores';
@ -10,50 +10,45 @@
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification, generateRemoteEngine } from '$lib/common';
import { appSession } from '$lib/store';
import { errorNotification, generateRemoteEngine } from '$lib/common';
import { appSession } from '$lib/store';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
let loading = false;
let restarting = false;
$: isDisabled = !$appSession.isAdmin;
async function handleSubmit() {
loading = true;
try {
return await post(`/destinations/${id}.json`, { ...destination });
} catch (error ) {
return await post(`/destinations/${id}`, { ...destination });
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
// async function scanApps() {
// scannedApps = [];
// const data = await fetch(`/destinations/${id}/scan.json`);
// const { containers } = await data.json();
// scannedApps = containers;
// }
onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error ) {
} catch (error) {
return errorNotification(error);
}
} else if (state === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch ( error ) {
} catch (error) {
return errorNotification(error);
}
}
@ -73,7 +68,7 @@ import { appSession } from '$lib/store';
}
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
@ -89,8 +84,7 @@ import { appSession } from '$lib/store';
}
async function stopProxy() {
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/stop.json`, { engine });
await post(`/destinations/${id}/stop`, { engine: destination.engine });
return toast.push($t('destination.coolify_proxy_stopped'));
} catch (error) {
return errorNotification(error);
@ -98,8 +92,7 @@ import { appSession } from '$lib/store';
}
async function startProxy() {
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/start.json`, { engine });
await post(`/destinations/${id}/start`, { engine: destination.engine });
return toast.push($t('destination.coolify_proxy_started'));
} catch (error) {
return errorNotification(error);
@ -111,7 +104,7 @@ import { appSession } from '$lib/store';
try {
restarting = true;
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
fqdn: settings.fqdn
});
@ -119,9 +112,21 @@ import { appSession } from '$lib/store';
setTimeout(() => {
window.location.reload();
}, 5000);
} finally {
restarting = false;
}
}
}
async function verifyRemoteDocker() {
try {
loading = true;
return await post(`/destinations/${id}/verify`, {});
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
@ -136,6 +141,11 @@ import { appSession } from '$lib/store';
disabled={loading}
>{loading ? $t('forms.saving') : $t('forms.save')}
</button>
{#if !destination.remoteVerified}
<button on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>Verify Remote Docker Engine</button
>
{/if}
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
@ -145,9 +155,6 @@ import { appSession } from '$lib/store';
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
@ -159,22 +166,6 @@ import { appSession } from '$lib/store';
bind:value={destination.name}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
<!-- <div class="flex items-center">
<label for="remoteEngine">Remote Engine?</label>
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
@ -186,6 +177,49 @@ import { appSession } from '$lib/store';
value={destination.network}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100">IP Address</label>
<CopyPasswordField
id="remoteIpAddress"
readonly
disabled
name="remoteIpAddress"
value={destination.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField
id="remoteUser"
readonly
disabled
name="remoteUser"
value={destination.remoteUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label>
<CopyPasswordField
id="remotePort"
readonly
disabled
name="remotePort"
value={destination.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshKey" class="text-base font-bold text-stone-100">SSH Key</label>
<a
href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''}
class="no-underline"
><input
value={destination.sshKey.name}
id="sshKey"
disabled
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
disabled={cannotDisable}
@ -200,27 +234,3 @@ import { appSession } from '$lib/store';
/>
</div>
</form>
<!-- <div class="flex justify-center">
{#if payload.isCoolifyProxyUsed}
{#if state}
<button on:click={stopProxy}>Stop proxy</button>
{:else}
<button on:click={startProxy}>Start proxy</button>
{/if}
{/if}
</div> -->
<!-- {#if scannedApps.length > 0}
<div class="flex justify-center px-6 pb-10">
<div class="flex space-x-2 h-8 items-center">
<div class="font-bold text-xl text-white">Found applications</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-6">
<div class="flex space-x-2 justify-center">
{#each scannedApps as app}
<FoundApp {app} />
{/each}
</div>
</div>
{/if} -->

View File

@ -1,6 +1,14 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, url, params }) => {
function checkConfiguration(destination: any): string | null {
let configurationPhase = null;
if (!destination?.remoteEngine) return configurationPhase;
if (!destination?.sshKey) {
configurationPhase = 'sshkey';
}
return configurationPhase;
}
export const load: Load = async ({ url, params }) => {
try {
const { id } = params;
const response = await get(`/destinations/${id}`);
@ -11,6 +19,17 @@
redirect: '/destinations'
};
}
const configurationPhase = checkConfiguration(destination);
if (
configurationPhase &&
url.pathname !== `/destinations/${params.id}/configuration/${configurationPhase}`
) {
return {
status: 302,
redirect: `/destinations/${params.id}/configuration/${configurationPhase}`
};
}
return {
props: {
destination
@ -22,6 +41,7 @@
}
};
} catch (error) {
console.log(error)
return handlerNotFoundLoad(error, url);
}
};

View File

@ -0,0 +1,59 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/settings`);
return {
props: {
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
export let sshKeys: any;
async function handleSubmit(sshKeyId: string) {
try {
await post(`/destinations/${id}/configuration/sshKey`, { id: sshKeyId });
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">SSH Keys</div>
</div>
<div class="flex flex-col justify-center">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row ">
{#each sshKeys as sshKey}
<div class="p-2 relative">
<form on:submit|preventDefault={() => handleSubmit(sshKey.id)}>
<button
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
>
<div class="font-bold text-xl text-center truncate">{sshKey.name}</div>
</button>
</form>
</div>
{/each}
</div>
</div>

View File

@ -73,6 +73,9 @@
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<div class="truncate text-center text-sm text-red-500">Not verified yet</div>
{/if}
</div>
</a>
{/each}

View File

@ -19,7 +19,7 @@
<script lang="ts">
export let settings: any;
export let sshKeys: any;
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { del, get, post } from '$lib/api';
@ -51,13 +51,20 @@
proxyMigration: false
};
let subMenuActive: any = 'globalsettings';
let isModalActive = false;
let newSSHKey = {
name: null,
privateKey: null
};
async function removeFqdn() {
if (fqdn) {
loading.remove = true;
try {
const { redirect } = await del(`/settings`, { fqdn });
return redirect ? window.location.replace(redirect) : window.location.reload();
} catch (error ) {
} catch (error) {
return errorNotification(error);
} finally {
loading.remove = false;
@ -107,7 +114,7 @@
settings.maxPort = maxPort;
}
forceSave = false;
} catch (error ) {
} catch (error) {
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
@ -122,7 +129,7 @@
}
}
}
console.log(error)
console.log(error);
return errorNotification(error);
} finally {
loading.save = false;
@ -143,18 +150,23 @@
function resetView() {
forceSave = false;
}
async function migrateProxy(to: any) {
if (loading.proxyMigration) return;
async function saveSSHKey() {
try {
loading.proxyMigration = true;
await post(`/update`, { type: to });
const data = await get(`/settings`);
$isTraefikUsed = data.settings.isTraefikUsed;
return toast.push('Proxy migration started, it takes a few seconds.');
await post(`/settings/sshKey`, { ...newSSHKey });
return window.location.reload();
} catch (error) {
return errorNotification(error);
} finally {
loading.proxyMigration = false;
errorNotification(error);
return false;
}
}
async function deleteSSHKey(id: string) {
try {
if (!id) return
await del(`/settings/sshKey`, { id });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
</script>
@ -162,196 +174,303 @@
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
{#if $appSession.teamId === '0'}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
class:bg-green-600={!loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-green-500={!loading.save}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
<div class="mx-auto w-full">
<div class="flex flex-row">
<div class="flex flex-col pt-4 space-y-6 w-96 px-20">
<div
class="sub-menu"
class:sub-menu-active={subMenuActive === 'globalsettings'}
on:click={() => (subMenuActive = 'globalsettings')}
>
Global Settings
</div>
<div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')}
</div>
<Explainer text={$t('setting.ssl_explainer')} />
</div>
<div class="justify-start text-left">
<input
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
<div
class="sub-menu"
class:sub-menu-active={subMenuActive === 'sshkey'}
on:click={() => (subMenuActive = 'sshkey')}
>
SSH Keys
</div>
</div>
<div class="pl-40">
{#if $appSession.teamId === '0'}
{#if subMenuActive === 'globalsettings'}
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
class:bg-yellow-500={!loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-yellow-500={!loading.save}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')}
</div>
<Explainer text={$t('setting.ssl_explainer')} />
</div>
<div class="justify-start text-left">
<input
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
{/if}
</div>
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')}
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')}
</div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
class="h-8 w-20 px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
-
<input
class="h-8 w-20 px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
class="h-8 w-20 px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
-
<input
class="h-8 w-20 px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
</form>
{#if !settings.isTraefikUsed}
<div class="flex space-x-1 pt-6 font-bold">
<div class="title">{$t('setting.coolify_proxy_settings')}</div>
</div>
<Explainer
text={$t('setting.credential_stat_explainer', {
link: fqdn
? `http://${settings.proxyUser}:${settings.proxyPassword}@` +
getDomain(fqdn) +
':8404'
: browser &&
`http://${settings.proxyUser}:${settings.proxyPassword}@` +
window.location.hostname +
':8404'
})}
/>
<div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-2 items-center">
<label for="proxyUser">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
id="proxyUser"
name="proxyUser"
value={settings.proxyUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="proxyPassword">{$t('forms.password')}</label>
<CopyPasswordField
readonly
disabled
id="proxyPassword"
name="proxyPassword"
isPasswordField
value={settings.proxyPassword}
/>
</div>
</div>
{/if}
{/if}
{#if subMenuActive === 'sshkey'}
<div class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">SSH Keys</div>
<button
on:click={() => (isModalActive = true)}
class:bg-yellow-500={!loading.save}
class:hover:bg-yellow-400={!loading.save}
disabled={loading.save}>New SSH Key</button
>
</div>
<div class="grid grid-flow-col gap-2 px-10">
{#if sshKeys.length === 0}
<div class="text-sm ">No SSH keys found</div>
{:else}
{#each sshKeys as key}
<div class="box-selection group relative">
<div class="text-xl font-bold">{key.name}</div>
<div class="py-3 text-stone-600">Added on {key.createdAt}</div>
<button on:click={() => deleteSSHKey(key.id)} class="bg-red-500">Delete</button>
</div>
{/each}
{/if}
</div>
</div>
{/if}
</div>
</form>
{#if !settings.isTraefikUsed}
<div class="flex space-x-1 pt-6 font-bold">
<div class="title">{$t('setting.coolify_proxy_settings')}</div>
{/if}
</div>
<Explainer
text={$t('setting.credential_stat_explainer', {
link: fqdn
? `http://${settings.proxyUser}:${settings.proxyPassword}@` + getDomain(fqdn) + ':8404'
: browser &&
`http://${settings.proxyUser}:${settings.proxyPassword}@` +
window.location.hostname +
':8404'
})}
/>
<div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-2 items-center">
<label for="proxyUser">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
id="proxyUser"
name="proxyUser"
value={settings.proxyUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="proxyPassword">{$t('forms.password')}</label>
<CopyPasswordField
readonly
disabled
id="proxyPassword"
name="proxyPassword"
isPasswordField
value={settings.proxyPassword}
/>
</div>
</div>
{/if}
</div>
{:else}
<div class="mx-auto max-w-4xl px-6">
<!-- <Language /> -->
</div>
{#if isModalActive}
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-coolgray-500 bg-opacity-75 transition-opacity" />
<div class="fixed z-10 inset-0 overflow-y-auto text-white">
<div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
<div
class="relative bg-coolblack rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full sm:p-6 border border-coolgray-500"
>
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button
on:click={() => (isModalActive = false)}
type="button"
class=" rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span class="sr-only">Close</span>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium pb-4" id="modal-title">New SSH Key</h3>
<div class="text-xs text-stone-400">Add an SSH key to your Coolify instance.</div>
<div class="mt-2">
<label for="privateKey" class="pb-2">Key</label>
<textarea
id="privateKey"
required
bind:value={newSSHKey.privateKey}
class="w-full"
rows={15}
/>
</div>
<div class="mt-2">
<label for="name" class="pb-2">Name</label>
<input id="name" required bind:value={newSSHKey.name} class="w-full" />
</div>
</div>
</div>
<div class="mt-5 flex space-x-4 justify-end">
<button on:click={saveSSHKey} type="button" class="bg-green-600 hover:bg-green-500"
>Save</button
>
<button on:click={() => (isModalActive = false)} type="button" class="">Cancel</button>
</div>
</div>
</div>
</div>
</div>
{/if}

View File

@ -395,3 +395,11 @@ a {
transform: scale(0.9);
}
}
.sub-menu {
@apply text-xl font-bold hover:bg-coolgray-500 rounded p-2 hover:text-white text-stone-200 cursor-pointer;
}
.sub-menu-active {
@apply bg-coolgray-500 text-white;
}

182
pnpm-lock.yaml generated
View File

@ -14,14 +14,14 @@ importers:
specifiers:
'@breejs/ts-worker': 2.0.0
'@fastify/autoload': 5.1.0
'@fastify/cookie': 7.1.0
'@fastify/cookie': 7.2.0
'@fastify/cors': 8.0.0
'@fastify/env': 4.0.0
'@fastify/jwt': 6.3.1
'@fastify/static': 6.4.0
'@iarna/toml': 2.2.5
'@prisma/client': 3.15.2
'@types/node': 18.0.4
'@types/node': 18.0.6
'@types/node-os-utils': 1.3.0
'@typescript-eslint/eslint-plugin': 5.30.6
'@typescript-eslint/parser': 5.30.6
@ -35,7 +35,7 @@ importers:
dockerode: 3.3.2
dotenv-extended: 2.9.0
esbuild: 0.14.49
eslint: 8.19.0
eslint: 8.20.0
eslint-config-prettier: 8.5.0
eslint-plugin-prettier: 4.2.1
fastify: 4.2.1
@ -53,15 +53,15 @@ importers:
prettier: 2.7.1
prisma: 3.15.2
rimraf: 3.0.2
shared: workspace:*
ssh-config: ^4.1.6
strip-ansi: 7.0.1
tsconfig-paths: 4.0.0
typescript: 4.7.4
unique-names-generator: 4.7.1
dependencies:
'@breejs/ts-worker': 2.0.0_t3dw2jfpvj5qtbx4qztd4nt754
'@breejs/ts-worker': 2.0.0_25g7irgsr6ywin2g3nrhhgteo4
'@fastify/autoload': 5.1.0
'@fastify/cookie': 7.1.0
'@fastify/cookie': 7.2.0
'@fastify/cors': 8.0.0
'@fastify/env': 4.0.0
'@fastify/jwt': 6.3.1
@ -88,18 +88,18 @@ importers:
node-forge: 1.3.1
node-os-utils: 1.3.7
p-queue: 7.2.0
shared: link:../shared
ssh-config: 4.1.6
strip-ansi: 7.0.1
unique-names-generator: 4.7.1
devDependencies:
'@types/node': 18.0.4
'@types/node': 18.0.6
'@types/node-os-utils': 1.3.0
'@typescript-eslint/eslint-plugin': 5.30.6_2vt5mtrqleafs33qg2bhpmbaqm
'@typescript-eslint/parser': 5.30.6_4x5o4skxv6sl53vpwefgt23khm
'@typescript-eslint/eslint-plugin': 5.30.6_b7n364ggt6o4xlkgyoaww3ph3q
'@typescript-eslint/parser': 5.30.6_he2ccbldppg44uulnyq4rwocfa
esbuild: 0.14.49
eslint: 8.19.0
eslint-config-prettier: 8.5.0_eslint@8.19.0
eslint-plugin-prettier: 4.2.1_7uxdfn2xinezdgvmbammh6ev5i
eslint: 8.20.0
eslint-config-prettier: 8.5.0_eslint@8.20.0
eslint-plugin-prettier: 4.2.1_g4fztgbwjyq2fvmcscny2sj6fy
nodemon: 2.0.19
prettier: 2.7.1
prisma: 3.15.2
@ -107,28 +107,18 @@ importers:
tsconfig-paths: 4.0.0
typescript: 4.7.4
apps/shared:
specifiers:
esbuild: 0.14.49
nodemon: 2.0.19
rimraf: 3.0.2
devDependencies:
esbuild: 0.14.49
nodemon: 2.0.19
rimraf: 3.0.2
apps/ui:
specifiers:
'@playwright/test': 1.23.3
'@sveltejs/adapter-static': 1.0.0-next.36
'@sveltejs/kit': 1.0.0-next.375
'@playwright/test': 1.23.4
'@sveltejs/adapter-static': 1.0.0-next.37
'@sveltejs/kit': 1.0.0-next.377
'@types/js-cookie': 3.0.2
'@typescript-eslint/eslint-plugin': 5.30.6
'@typescript-eslint/parser': 5.30.6
'@zerodevx/svelte-toast': 0.7.2
autoprefixer: 10.4.7
cuid: 2.1.8
eslint: 8.19.0
eslint: 8.20.0
eslint-config-prettier: 8.5.0
eslint-plugin-svelte3: 4.0.0
js-cookie: 3.0.1
@ -136,7 +126,6 @@ importers:
postcss: 8.4.14
prettier: 2.7.1
prettier-plugin-svelte: 2.7.0
shared: workspace:*
svelte: 3.49.0
svelte-check: 2.8.0
svelte-preprocess: 4.10.7
@ -146,26 +135,25 @@ importers:
tailwindcss-scrollbar: 0.1.0
tslib: 2.4.0
typescript: 4.7.4
vite: ^3.0.0
vite: 3.0.1
dependencies:
'@sveltejs/adapter-static': 1.0.0-next.36
'@sveltejs/adapter-static': 1.0.0-next.37
'@zerodevx/svelte-toast': 0.7.2
cuid: 2.1.8
js-cookie: 3.0.1
p-limit: 4.0.0
shared: link:../shared
svelte-select: 4.4.7
sveltekit-i18n: 2.2.2_svelte@3.49.0
devDependencies:
'@playwright/test': 1.23.3
'@sveltejs/kit': 1.0.0-next.375_svelte@3.49.0+vite@3.0.0
'@playwright/test': 1.23.4
'@sveltejs/kit': 1.0.0-next.377_svelte@3.49.0+vite@3.0.1
'@types/js-cookie': 3.0.2
'@typescript-eslint/eslint-plugin': 5.30.6_2vt5mtrqleafs33qg2bhpmbaqm
'@typescript-eslint/parser': 5.30.6_4x5o4skxv6sl53vpwefgt23khm
'@typescript-eslint/eslint-plugin': 5.30.6_b7n364ggt6o4xlkgyoaww3ph3q
'@typescript-eslint/parser': 5.30.6_he2ccbldppg44uulnyq4rwocfa
autoprefixer: 10.4.7_postcss@8.4.14
eslint: 8.19.0
eslint-config-prettier: 8.5.0_eslint@8.19.0
eslint-plugin-svelte3: 4.0.0_jxmmfmurkts274jdspwh3cyqve
eslint: 8.20.0
eslint-config-prettier: 8.5.0_eslint@8.20.0
eslint-plugin-svelte3: 4.0.0_piwa6j2njmnknm35bh3wz5v52y
postcss: 8.4.14
prettier: 2.7.1
prettier-plugin-svelte: 2.7.0_o3ioganyptcsrh6x4hnxvjkpqi
@ -176,7 +164,7 @@ importers:
tailwindcss-scrollbar: 0.1.0_tailwindcss@3.1.6
tslib: 2.4.0
typescript: 4.7.4
vite: 3.0.0
vite: 3.0.1
packages:
@ -213,14 +201,14 @@ packages:
engines: {node: '>= 10'}
dev: false
/@breejs/ts-worker/2.0.0_t3dw2jfpvj5qtbx4qztd4nt754:
/@breejs/ts-worker/2.0.0_25g7irgsr6ywin2g3nrhhgteo4:
resolution: {integrity: sha512-6anHRcmgYlF7mrm/YVRn6rx2cegLuiY3VBxkkimOTWC/dVQeH336imVSuIKEGKTwiuNTPr2hswVdDSneNuXg3A==}
engines: {node: '>= 12.11'}
peerDependencies:
bree: '>=9.0.0'
dependencies:
bree: 9.1.1
ts-node: 10.8.2_2zqz24ol5yhbv2blv4fh7akzrq
ts-node: 10.8.2_tdn3ypgnfy6bmey2q4hu5jonwi
tsconfig-paths: 4.0.0
transitivePeerDependencies:
- '@swc/core'
@ -267,8 +255,8 @@ packages:
pkg-up: 3.1.0
dev: false
/@fastify/cookie/7.1.0:
resolution: {integrity: sha512-ofAlIthvJ2aWOrzdbUen1Lx09AKk2zvdaUrWh2+0aNt+gajRA7KyR8bzwCD2AwS+2nacjEuSRIyckotMHG95hQ==}
/@fastify/cookie/7.2.0:
resolution: {integrity: sha512-eM/OoTPEW/83uTEWVVZhVVQCtwRx3vmMs7J68U1DFNf42Ar4nTTZ7qGNYXvJPLUQqGKYS/gxML2soNMmZD8z0Q==}
dependencies:
cookie: 0.5.0
cookie-signature: 1.2.0
@ -385,13 +373,13 @@ packages:
fastq: 1.13.0
dev: true
/@playwright/test/1.23.3:
resolution: {integrity: sha512-kR4vo2UGHC84DGqE6EwvAeaehj3xCAK6LoC1P1eu6ZGdC79rlqRKf8cFDx6q6c9T8MQSL1O9eOlup0BpwqNF0w==}
/@playwright/test/1.23.4:
resolution: {integrity: sha512-iIsoMJDS/lyuhw82FtcV/B3PXikgVD3hNe5hyvOpRM0uRr1OIpN3LgPeRbBjhzBWmyf6RgRg5fqK5sVcpA03yA==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@types/node': 18.0.3
playwright-core: 1.23.3
'@types/node': 18.0.4
playwright-core: 1.23.4
dev: true
/@prisma/client/3.15.2_prisma@3.15.2:
@ -429,31 +417,31 @@ packages:
engines: {node: '>=10'}
dev: false
/@sveltejs/adapter-static/1.0.0-next.36:
resolution: {integrity: sha512-1g3W4wHPyBtUGy5zCDBA2nMG3mM36FKTP1zb0vNRBpoUmtNuzVFF74UVsHCpMC1GpPyrgOq9idfjkm4gRabisw==}
/@sveltejs/adapter-static/1.0.0-next.37:
resolution: {integrity: sha512-BDFkx4CGAd6pG4e3+zYjy/eM9UDbhkRgXqavUzCO5oT8xXao5TeprY1AIbdzjMTmFjsWdeSXE9TbIsT0iikpyQ==}
dependencies:
tiny-glob: 0.2.9
dev: false
/@sveltejs/kit/1.0.0-next.375_svelte@3.49.0+vite@3.0.0:
resolution: {integrity: sha512-9+gKm97TW/xIz6DfWOqdsIwGY4yckUkmMFlsJmEGkjcTy60Q6ZCfrQhMULzL/fILLydF0wZcD/fWE/urAbp2nw==}
/@sveltejs/kit/1.0.0-next.377_svelte@3.49.0+vite@3.0.1:
resolution: {integrity: sha512-DH2v2yUBUuDZ7vzjPXUd/yt1AMR3BIkZN0ubLAvS2C+q5Wbvk7ZvAJhfPZ3OYc3ZpQXe4ZGEcptOjvEYvd1lLA==}
engines: {node: '>=16.9'}
hasBin: true
peerDependencies:
svelte: ^3.44.0
vite: ^3.0.0
dependencies:
'@sveltejs/vite-plugin-svelte': 1.0.1_svelte@3.49.0+vite@3.0.0
'@sveltejs/vite-plugin-svelte': 1.0.1_svelte@3.49.0+vite@3.0.1
chokidar: 3.5.3
sade: 1.8.1
svelte: 3.49.0
vite: 3.0.0
vite: 3.0.1
transitivePeerDependencies:
- diff-match-patch
- supports-color
dev: true
/@sveltejs/vite-plugin-svelte/1.0.1_svelte@3.49.0+vite@3.0.0:
/@sveltejs/vite-plugin-svelte/1.0.1_svelte@3.49.0+vite@3.0.1:
resolution: {integrity: sha512-PorCgUounn0VXcpeJu+hOweZODKmGuLHsLomwqSj+p26IwjjGffmYQfVHtiTWq+NqaUuuHWWG7vPge6UFw4Aeg==}
engines: {node: ^14.18.0 || >= 16}
peerDependencies:
@ -471,7 +459,7 @@ packages:
magic-string: 0.26.2
svelte: 3.49.0
svelte-hmr: 0.14.12_svelte@3.49.0
vite: 3.0.0
vite: 3.0.1
transitivePeerDependencies:
- supports-color
dev: true
@ -518,7 +506,7 @@ packages:
dependencies:
'@types/http-cache-semantics': 4.0.1
'@types/keyv': 3.1.4
'@types/node': 18.0.4
'@types/node': 18.0.6
'@types/responselike': 1.0.0
dev: false
@ -541,7 +529,7 @@ packages:
/@types/keyv/3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
'@types/node': 18.0.4
'@types/node': 18.0.6
dev: false
/@types/lodash/4.14.182:
@ -552,12 +540,12 @@ packages:
resolution: {integrity: sha512-XwVteWQx/XkfRPyaGkw8dEbrCAkoRZ73pI3XznUYIpzbCfpQB3UnDlR5TnmdhetlT889tUJGF8QWo9xrgTpsiA==}
dev: true
/@types/node/18.0.3:
resolution: {integrity: sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==}
dev: true
/@types/node/18.0.4:
resolution: {integrity: sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==}
dev: true
/@types/node/18.0.6:
resolution: {integrity: sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==}
/@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
@ -570,16 +558,16 @@ packages:
/@types/responselike/1.0.0:
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
dependencies:
'@types/node': 18.0.4
'@types/node': 18.0.6
dev: false
/@types/sass/1.43.1:
resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
dependencies:
'@types/node': 18.0.3
'@types/node': 18.0.4
dev: true
/@typescript-eslint/eslint-plugin/5.30.6_2vt5mtrqleafs33qg2bhpmbaqm:
/@typescript-eslint/eslint-plugin/5.30.6_b7n364ggt6o4xlkgyoaww3ph3q:
resolution: {integrity: sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -590,12 +578,12 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/parser': 5.30.6_4x5o4skxv6sl53vpwefgt23khm
'@typescript-eslint/parser': 5.30.6_he2ccbldppg44uulnyq4rwocfa
'@typescript-eslint/scope-manager': 5.30.6
'@typescript-eslint/type-utils': 5.30.6_4x5o4skxv6sl53vpwefgt23khm
'@typescript-eslint/utils': 5.30.6_4x5o4skxv6sl53vpwefgt23khm
'@typescript-eslint/type-utils': 5.30.6_he2ccbldppg44uulnyq4rwocfa
'@typescript-eslint/utils': 5.30.6_he2ccbldppg44uulnyq4rwocfa
debug: 4.3.4
eslint: 8.19.0
eslint: 8.20.0
functional-red-black-tree: 1.0.1
ignore: 5.2.0
regexpp: 3.2.0
@ -606,7 +594,7 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser/5.30.6_4x5o4skxv6sl53vpwefgt23khm:
/@typescript-eslint/parser/5.30.6_he2ccbldppg44uulnyq4rwocfa:
resolution: {integrity: sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -620,7 +608,7 @@ packages:
'@typescript-eslint/types': 5.30.6
'@typescript-eslint/typescript-estree': 5.30.6_typescript@4.7.4
debug: 4.3.4
eslint: 8.19.0
eslint: 8.20.0
typescript: 4.7.4
transitivePeerDependencies:
- supports-color
@ -634,7 +622,7 @@ packages:
'@typescript-eslint/visitor-keys': 5.30.6
dev: true
/@typescript-eslint/type-utils/5.30.6_4x5o4skxv6sl53vpwefgt23khm:
/@typescript-eslint/type-utils/5.30.6_he2ccbldppg44uulnyq4rwocfa:
resolution: {integrity: sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -644,9 +632,9 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/utils': 5.30.6_4x5o4skxv6sl53vpwefgt23khm
'@typescript-eslint/utils': 5.30.6_he2ccbldppg44uulnyq4rwocfa
debug: 4.3.4
eslint: 8.19.0
eslint: 8.20.0
tsutils: 3.21.0_typescript@4.7.4
typescript: 4.7.4
transitivePeerDependencies:
@ -679,7 +667,7 @@ packages:
- supports-color
dev: true
/@typescript-eslint/utils/5.30.6_4x5o4skxv6sl53vpwefgt23khm:
/@typescript-eslint/utils/5.30.6_he2ccbldppg44uulnyq4rwocfa:
resolution: {integrity: sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -689,9 +677,9 @@ packages:
'@typescript-eslint/scope-manager': 5.30.6
'@typescript-eslint/types': 5.30.6
'@typescript-eslint/typescript-estree': 5.30.6_typescript@4.7.4
eslint: 8.19.0
eslint: 8.20.0
eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.19.0
eslint-utils: 3.0.0_eslint@8.20.0
transitivePeerDependencies:
- supports-color
- typescript
@ -2531,16 +2519,16 @@ packages:
engines: {node: '>=10'}
dev: true
/eslint-config-prettier/8.5.0_eslint@8.19.0:
/eslint-config-prettier/8.5.0_eslint@8.20.0:
resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
dependencies:
eslint: 8.19.0
eslint: 8.20.0
dev: true
/eslint-plugin-prettier/4.2.1_7uxdfn2xinezdgvmbammh6ev5i:
/eslint-plugin-prettier/4.2.1_g4fztgbwjyq2fvmcscny2sj6fy:
resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
@ -2551,19 +2539,19 @@ packages:
eslint-config-prettier:
optional: true
dependencies:
eslint: 8.19.0
eslint-config-prettier: 8.5.0_eslint@8.19.0
eslint: 8.20.0
eslint-config-prettier: 8.5.0_eslint@8.20.0
prettier: 2.7.1
prettier-linter-helpers: 1.0.0
dev: true
/eslint-plugin-svelte3/4.0.0_jxmmfmurkts274jdspwh3cyqve:
/eslint-plugin-svelte3/4.0.0_piwa6j2njmnknm35bh3wz5v52y:
resolution: {integrity: sha512-OIx9lgaNzD02+MDFNLw0GEUbuovNcglg+wnd/UY0fbZmlQSz7GlQiQ1f+yX0XvC07XPcDOnFcichqI3xCwp71g==}
peerDependencies:
eslint: '>=8.0.0'
svelte: ^3.2.0
dependencies:
eslint: 8.19.0
eslint: 8.20.0
svelte: 3.49.0
dev: true
@ -2583,13 +2571,13 @@ packages:
estraverse: 5.3.0
dev: true
/eslint-utils/3.0.0_eslint@8.19.0:
/eslint-utils/3.0.0_eslint@8.20.0:
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
peerDependencies:
eslint: '>=5'
dependencies:
eslint: 8.19.0
eslint: 8.20.0
eslint-visitor-keys: 2.1.0
dev: true
@ -2603,8 +2591,8 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/eslint/8.19.0:
resolution: {integrity: sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==}
/eslint/8.20.0:
resolution: {integrity: sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
hasBin: true
dependencies:
@ -2617,7 +2605,7 @@ packages:
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.19.0
eslint-utils: 3.0.0_eslint@8.20.0
eslint-visitor-keys: 3.3.0
espree: 9.3.2
esquery: 1.4.0
@ -4259,8 +4247,8 @@ packages:
find-up: 3.0.0
dev: false
/playwright-core/1.23.3:
resolution: {integrity: sha512-x35yzsXDyo0BIXYimLnUFNyb42c//NadUNH6IPGOteZm96oTGA1kn4Hq6qJTI1/f9wEc1F9O1DsznXIgXMil7A==}
/playwright-core/1.23.4:
resolution: {integrity: sha512-h5V2yw7d8xIwotjyNrkLF13nV9RiiZLHdXeHo+nVJIYGVlZ8U2qV0pMxNJKNTvfQVT0N8/A4CW6/4EW2cOcTiA==}
engines: {node: '>=14'}
hasBin: true
dev: true
@ -4860,6 +4848,10 @@ packages:
engines: {node: '>= 10.x'}
dev: false
/ssh-config/4.1.6:
resolution: {integrity: sha512-YdPYn/2afoBonSFoMSvC1FraA/LKKrvy8UvbvAFGJ8gdlKuANvufLLkf8ynF2uq7Tl5+DQBIFyN37//09nAgNQ==}
dev: false
/ssh2/1.10.0:
resolution: {integrity: sha512-OnKAAmf4j8wCRrXXZv3Tp5lCZkLJZtgZbn45ELiShCg27djDQ3XFGvIzuGsIsf4hdHslP+VdhA9BhUQdTdfd9w==}
engines: {node: '>=10.16.0'}
@ -5217,7 +5209,7 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/ts-node/10.8.2_2zqz24ol5yhbv2blv4fh7akzrq:
/ts-node/10.8.2_tdn3ypgnfy6bmey2q4hu5jonwi:
resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==}
hasBin: true
peerDependencies:
@ -5236,7 +5228,7 @@ packages:
'@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 18.0.4
'@types/node': 18.0.6
acorn: 8.7.1
acorn-walk: 8.2.0
arg: 4.1.3
@ -5373,9 +5365,9 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/vite/3.0.0:
resolution: {integrity: sha512-M7phQhY3+fRZa0H+1WzI6N+/onruwPTBTMvaj7TzgZ0v2TE+N2sdLKxJOfOv9CckDWt5C4HmyQP81xB4dwRKzA==}
engines: {node: '>=14.18.0'}
/vite/3.0.1:
resolution: {integrity: sha512-nefKSglkoEsDpYUkBuT2++L04ktcP8fz8dxLtmZdDdMyhubFSOLFw6BTh/46Fc6tIX/cibs/NVYWNrsqn0k6pQ==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
less: '*'