diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 33748315c..aac716a58 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -317,12 +317,16 @@ export function getDomain(domain: string): string { export async function isDomainConfigured({ id, fqdn, - checkOwn = false + checkOwn = false, + dockerId = undefined }: { id: string; fqdn: string; checkOwn?: boolean; + dockerId: string; }): Promise { + + console.log({checkOwn, dockerId}) const domain = getDomain(fqdn); const nakedDomain = domain.replace('www.', ''); const foundApp = await prisma.application.findFirst({ @@ -331,7 +335,10 @@ export async function isDomainConfigured({ { fqdn: { endsWith: `//${nakedDomain}` } }, { fqdn: { endsWith: `//www.${nakedDomain}` } } ], - id: { not: id } + id: { not: id }, + destinationDocker: { + id: dockerId + } }, select: { fqdn: true } }); @@ -343,7 +350,10 @@ export async function isDomainConfigured({ { minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } }, { minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } } ], - id: { not: checkOwn ? undefined : id } + id: { not: checkOwn ? undefined : id }, + destinationDocker: { + id: dockerId + } }, select: { fqdn: true } }); @@ -416,16 +426,13 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P } } else { try { - console.log({domain}) const ipDomain = await dns.resolve4(domain); - console.log({ipDomain}) let ipDomainFound = false; for (const ip of ipDomain) { if (resolves.includes(ip)) { ipDomainFound = true; } } - console.log({ipDomainFound}) if (ipDomainFound) return { status: 200 }; throw { status: 500, message: `DNS not set correctly or propogated.
Please check your DNS settings.` } } catch (error) { diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 0d3606187..5896422ca 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -368,7 +368,7 @@ export async function checkDNS(request: FastifyRequest) { const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); - const found = await isDomainConfigured({ id, fqdn }); + const found = await isDomainConfigured({ id, fqdn, dockerId }); if (found) { throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } } diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index a8214a56a..1096e6681 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -2,13 +2,13 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; -import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort } from '../../../../lib/common'; +import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, dockerInstance, isContainerExited, removeContainer } from '../../../../lib/docker'; import cuid from 'cuid'; import type { OnlyId } from '../../../../types'; -import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; +import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; // async function startServiceNew(request: FastifyRequest) { // try { @@ -346,22 +346,35 @@ export async function saveServiceSettings(request: FastifyRequest) { + try { + const { id } = request.params + const { domain } = request.query + const { fqdn, dualCerts } = await prisma.service.findUnique({ where: { id }}) + return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts }); + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} export async function checkService(request: FastifyRequest) { try { const { id } = request.params; - let { fqdn, exposePort, otherFqdns } = request.body; + let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body; if (fqdn) fqdn = fqdn.toLowerCase(); if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase()); if (exposePort) exposePort = Number(exposePort); - let found = await isDomainConfigured({ id, fqdn }); + const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } }) + const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); + + let found = await isDomainConfigured({ id, fqdn, dockerId }); if (found) { throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } } if (otherFqdns && otherFqdns.length > 0) { for (const ofqdn of otherFqdns) { - found = await isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true }); + found = await isDomainConfigured({ id, fqdn: ofqdn, dockerId }); if (found) { throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` } } @@ -371,7 +384,7 @@ export async function checkService(request: FastifyRequest) { if (exposePort < 1024 || exposePort > 65535) { throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } } - const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } }) + if (configuredPort !== exposePort) { const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); if (availablePort.toString() !== exposePort.toString()) { @@ -379,6 +392,11 @@ export async function checkService(request: FastifyRequest) { } } } + if (isDNSCheckEnabled && !isDev && !forceSave) { + let hostname = request.hostname.split(':')[0]; + if (remoteEngine) hostname = remoteIpAddress; + return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); + } return {} } catch ({ status, message }) { return errorHandler({ status, message }) diff --git a/apps/api/src/routes/api/v1/services/index.ts b/apps/api/src/routes/api/v1/services/index.ts index e2192e979..bb5ab7670 100644 --- a/apps/api/src/routes/api/v1/services/index.ts +++ b/apps/api/src/routes/api/v1/services/index.ts @@ -3,6 +3,7 @@ import { activatePlausibleUsers, activateWordpressFtp, checkService, + checkServiceDomain, deleteService, deleteServiceSecret, deleteServiceStorage, @@ -29,7 +30,7 @@ import { } from './handlers'; import type { OnlyId } from '../../../../types'; -import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; +import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { fastify.addHook('onRequest', async (request) => { @@ -44,6 +45,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/status', async (request) => await getServiceStatus(request)); + fastify.get('/:id/check', async (request) => await checkServiceDomain(request)); fastify.post('/:id/check', async (request) => await checkService(request)); fastify.post('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply)); diff --git a/apps/api/src/routes/api/v1/services/types.ts b/apps/api/src/routes/api/v1/services/types.ts index 4ed631998..f09b4423f 100644 --- a/apps/api/src/routes/api/v1/services/types.ts +++ b/apps/api/src/routes/api/v1/services/types.ts @@ -25,9 +25,16 @@ export interface SaveServiceSettings extends OnlyId { dualCerts: boolean } } +export interface CheckServiceDomain extends OnlyId { + Querystring: { + domain: string + } +} export interface CheckService extends OnlyId { Body: { fqdn: string, + forceSave: boolean, + dualCerts: boolean, exposePort: number, otherFqdns: Array } diff --git a/apps/ui/src/routes/services/[id]/_Services/_Services.svelte b/apps/ui/src/routes/services/[id]/_Services/_Services.svelte index 682f8526c..a7b0c668c 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Services.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_Services.svelte @@ -11,7 +11,7 @@ import { toast } from '@zerodevx/svelte-toast'; import { get, post } from '$lib/api'; - import { errorNotification } from '$lib/common'; + import { errorNotification, getDomain } from '$lib/common'; import { t } from '$lib/translations'; import { appSession, disabledButton, status, location, setLocation } from '$lib/store'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; @@ -30,28 +30,63 @@ import Moodle from './_Moodle.svelte'; const { id } = $page.params; - $: isDisabled = !$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading; + let forceSave = false; let loading = false; let loadingVerification = false; let dualCerts = service.dualCerts; + let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, ''); + let isNonWWWDomainOK = false; + let isWWWDomainOK = false; + + async function isDNSValid(domain: any, isWWW: any) { + try { + await get(`/services/${id}/check?domain=${domain}`); + toast.push('DNS configuration is valid.'); + isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true); + return true; + } catch (error) { + errorNotification(error); + isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false); + return false; + } + } + async function handleSubmit() { if (loading) return; loading = true; try { await post(`/services/${id}/check`, { fqdn: service.fqdn, + forceSave, + dualCerts, otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [], exposePort: service.exposePort }); await post(`/services/${id}`, { ...service }); setLocation(service); $disabledButton = false; + forceSave = false; toast.push('Configuration saved.'); } catch (error) { + //@ts-ignore + if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) { + forceSave = true; + if (dualCerts) { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + const isWWW = getDomain(service.fqdn).includes('www.'); + if (isWWW) { + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + } + } + } return errorNotification(error); } finally { loading = false; @@ -111,8 +146,15 @@ {loading + ? $t('forms.saving') + : forceSave + ? $t('forms.confirm_continue') + : $t('forms.save')} {/if} {#if service.type === 'plausibleanalytics' && $status.service.isRunning} @@ -235,7 +277,38 @@ /> {/if} - + {#if forceSave} +
+ {#if isNonWWWDomainOK} + + {:else} + + {/if} + {#if dualCerts} + {#if isWWWDomainOK} + + {:else} + + {/if} + {/if} +
+ {/if}