diff --git a/.vscode/settings.json b/.vscode/settings.json index 65a483d79..11610a6f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,14 @@ "ts", "json" ], - "i18n-ally.extract.autoDetect": true + "i18n-ally.extract.autoDetect": true, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + "hide-files.files": [] } \ No newline at end of file diff --git a/apps/api/prisma/migrations/20230515060951_basic_auth/migration.sql b/apps/api/prisma/migrations/20230515060951_basic_auth/migration.sql new file mode 100644 index 000000000..239db7ea4 --- /dev/null +++ b/apps/api/prisma/migrations/20230515060951_basic_auth/migration.sql @@ -0,0 +1,29 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "basicAuthPw" TEXT; +ALTER TABLE "Application" ADD COLUMN "basicAuthUser" TEXT; + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_ApplicationSettings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "debug" BOOLEAN NOT NULL DEFAULT false, + "previews" BOOLEAN NOT NULL DEFAULT false, + "autodeploy" BOOLEAN NOT NULL DEFAULT true, + "isBot" BOOLEAN NOT NULL DEFAULT false, + "isPublicRepository" BOOLEAN NOT NULL DEFAULT false, + "isDBBranching" BOOLEAN NOT NULL DEFAULT false, + "isCustomSSL" BOOLEAN NOT NULL DEFAULT false, + "isHttp2" BOOLEAN NOT NULL DEFAULT false, + "basicAuth" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings"; +DROP TABLE "ApplicationSettings"; +ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; +CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 7dca8314b..62ff82afc 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -135,6 +135,8 @@ model Application { dockerRegistryId String? dockerRegistryImageName String? simpleDockerfile String? + basicAuthUser String? + basicAuthPw String? persistentStorage ApplicationPersistentStorage[] secrets Secret[] @@ -187,6 +189,7 @@ model ApplicationSettings { isDBBranching Boolean @default(false) isCustomSSL Boolean @default(false) isHttp2 Boolean @default(false) + basicAuth Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt application Application @relation(fields: [applicationId], references: [id]) diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index da10e119b..7d2c6ee8c 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -504,12 +504,17 @@ export async function saveApplicationSettings( isBot, isDBBranching, isCustomSSL, - isHttp2 + isHttp2, + basicAuth, + basicAuthUser, + basicAuthPw } = request.body; await prisma.application.update({ where: { id }, data: { fqdn: isBot ? null : undefined, + basicAuthUser, + basicAuthPw, settings: { update: { debug, @@ -519,7 +524,8 @@ export async function saveApplicationSettings( isBot, isDBBranching, isCustomSSL, - isHttp2 + isHttp2, + basicAuth, } } }, diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 1c42f468a..cfabf8043 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -43,6 +43,9 @@ export interface SaveApplicationSettings extends OnlyId { isDBBranching: boolean; isCustomSSL: boolean; isHttp2: boolean; + basicAuth: boolean; + basicAuthUser: string; + basicAuthPw: string; }; } export interface DeleteApplication extends OnlyId { diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index d39a33064..81adb18d5 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -1,5 +1,5 @@ import { FastifyRequest } from 'fastify'; -import { errorHandler, getDomain, isDev, prisma, executeCommand } from '../../../lib/common'; +import { errorHandler, executeCommand, getDomain, isDev, prisma } from '../../../lib/common'; import { getTemplates } from '../../../lib/services'; import { OnlyId } from '../../../types'; import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers'; @@ -48,20 +48,30 @@ function generateRouters( isWWW, isDualCerts, isCustomSSL, - isHttp2 = false + isHttp2 = false, + basicAuth = false, + basicAuthUser = '', + basicAuthPw = '' ) { - let rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`; - let ruleWWW = `Host(\`www.${nakedDomain}\`)${ + const rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`; + const ruleWWW = `Host(\`www.${nakedDomain}\`)${ pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : '' }`; - let http: any = { + + const httpBasicAuth: any = { + basicauth: { + users: [Buffer.from(basicAuthUser + ':' + basicAuthPw).toString('base64')] + } + }; + + const http: any = { entrypoints: ['web'], rule, service: `${serviceId}`, priority: 2, middlewares: [] }; - let https: any = { + const https: any = { entrypoints: ['websecure'], rule, service: `${serviceId}`, @@ -71,14 +81,14 @@ function generateRouters( }, middlewares: [] }; - let httpWWW: any = { + const httpWWW: any = { entrypoints: ['web'], rule: ruleWWW, service: `${serviceId}`, priority: 2, middlewares: [] }; - let httpsWWW: any = { + const httpsWWW: any = { entrypoints: ['websecure'], rule: ruleWWW, service: `${serviceId}`, @@ -97,6 +107,10 @@ function generateRouters( httpsWWW.middlewares.push('redirect-to-non-www'); delete https.tls; delete httpsWWW.tls; + + if (basicAuth) { + http.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`); + } } // 3. http + www only @@ -108,6 +122,10 @@ function generateRouters( https.middlewares.push('redirect-to-www'); delete https.tls; delete httpsWWW.tls; + + if (basicAuth) { + httpWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`); + } } // 5. https + non-www only if (isHttps && !isWWW) { @@ -136,6 +154,10 @@ function generateRouters( }; } } + + if (basicAuth) { + https.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`); + } } // 6. https + www only if (isHttps && isWWW) { @@ -145,6 +167,11 @@ function generateRouters( http.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www'); } + + if (basicAuth) { + httpsWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`); + } + if (isCustomSSL) { if (isDualCerts) { https.tls = true; @@ -166,23 +193,23 @@ function generateRouters( } } if (isHttp2) { - let http2 = { + const http2 = { ...http, service: `${serviceId}-http2`, rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` }; - let http2WWW = { + const http2WWW = { ...httpWWW, service: `${serviceId}-http2`, rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` }; - let https2 = { + const https2 = { ...https, service: `${serviceId}-http2`, rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` }; - let https2WWW = { + const https2WWW = { ...httpsWWW, service: `${serviceId}-http2`, rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` @@ -198,14 +225,21 @@ function generateRouters( [`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW } }; } - return { + + const result = { [`${serviceId}-${pathPrefix}`]: { ...http }, [`${serviceId}-${pathPrefix}-secure`]: { ...https }, [`${serviceId}-${pathPrefix}-www`]: { ...httpWWW }, [`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW } }; + + if (basicAuth) { + result[`${serviceId}-${pathPrefix}-basic-auth`] = { ...httpBasicAuth }; + } + + return result; } -export async function proxyConfiguration(request: FastifyRequest, remote: boolean = false) { +export async function proxyConfiguration(request: FastifyRequest, remote = false) { const traefik = { tls: { certificates: [] @@ -298,7 +332,7 @@ export async function proxyConfiguration(request: FastifyRequest, remote }); } - let parsedCertificates = []; + const parsedCertificates = []; for (const certificate of certificates) { parsedCertificates.push({ certFile: `${sslpath}/${certificate.id}-cert.pem`, @@ -369,7 +403,9 @@ export async function proxyConfiguration(request: FastifyRequest, remote dockerComposeConfiguration, destinationDocker, destinationDockerId, - settings + settings, + basicAuthUser, + basicAuthPw } = application; if (!destinationDockerId) { continue; @@ -424,7 +460,7 @@ export async function proxyConfiguration(request: FastifyRequest, remote } continue; } - const { previews, dualCerts, isCustomSSL, isHttp2 } = settings; + const { previews, dualCerts, isCustomSSL, isHttp2, basicAuth } = settings; const { network, id: dockerId } = destinationDocker; if (!fqdn) { continue; @@ -446,7 +482,10 @@ export async function proxyConfiguration(request: FastifyRequest, remote isWWW, dualCerts, isCustomSSL, - isHttp2 + isHttp2, + basicAuth, + basicAuthUser, + basicAuthPw ) }; traefik.http.services = { @@ -482,7 +521,11 @@ export async function proxyConfiguration(request: FastifyRequest, remote isHttps, isWWW, dualCerts, - isCustomSSL + isCustomSSL, + false, + basicAuth, + basicAuthUser, + basicAuthPw ) }; traefik.http.services = { @@ -542,7 +585,7 @@ export async function proxyConfiguration(request: FastifyRequest, remote if (isDomainAndProxyConfiguration.length > 0) { const template: any = await parseAndFindServiceTemplates(service, null, true); const { proxy } = template.services[oneService] || found.services[oneService]; - for (let configuration of proxy) { + for (const configuration of proxy) { if (configuration.hostPort) { continue; } diff --git a/apps/ui/src/lib/locales/en.json b/apps/ui/src/lib/locales/en.json index f04ae6d8e..158aef606 100644 --- a/apps/ui/src/lib/locales/en.json +++ b/apps/ui/src/lib/locales/en.json @@ -196,6 +196,9 @@ "domain_fqdn": "Domain (FQDN)", "https_explainer": "If you specify https, the application will be accessible only over https. SSL certificate will be generated for you.
If you specify www, the application will be redirected (302) from non-www and vice versa.

To modify the domain, you must first stop the application.

You must set your DNS to point to the server IP in advance.", "ssl_www_and_non_www": "Generate SSL for www and non-www?", + "basic_auth": "Basic Auth", + "basic_auth_user": "User", + "basic_auth_pw": "Password", "ssl_explainer": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.

Useful if you expect to have visitors on both.", "install_command": "Install Command", "build_command": "Build Command", diff --git a/apps/ui/src/routes/applications/[id]/index.svelte b/apps/ui/src/routes/applications/[id]/index.svelte index 85910ec2a..14524db99 100644 --- a/apps/ui/src/routes/applications/[id]/index.svelte +++ b/apps/ui/src/routes/applications/[id]/index.svelte @@ -29,27 +29,27 @@ export let application: any; export let settings: any; - import yaml from 'js-yaml'; + import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import { onMount } from 'svelte'; - import Select from 'svelte-select'; import { get, getAPIUrl, post } from '$lib/api'; - import cuid from 'cuid'; + import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; + import Beta from '$lib/components/Beta.svelte'; + import Explainer from '$lib/components/Explainer.svelte'; + import Setting from '$lib/components/Setting.svelte'; import { addToast, appSession, checkIfDeploymentEnabledApplications, - setLocation, - status, + features, isDeploymentEnabled, - features + setLocation, + status } from '$lib/store'; import { t } from '$lib/translations'; - import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; - import Setting from '$lib/components/Setting.svelte'; - import Explainer from '$lib/components/Explainer.svelte'; - import { goto } from '$app/navigation'; - import Beta from '$lib/components/Beta.svelte'; + import cuid from 'cuid'; + import yaml from 'js-yaml'; + import { onMount } from 'svelte'; + import Select from 'svelte-select'; import { saveForm } from './utils'; const { id } = $page.params; @@ -77,6 +77,7 @@ let isCustomSSL = application.settings?.isCustomSSL; let autodeploy = application.settings?.autodeploy; let isBot = application.settings?.isBot; + let basicAuth = application.settings?.basicAuth; let isDBBranching = application.settings?.isDBBranching; let htmlUrl = application.gitSource?.htmlUrl; let isHttp2 = application.settings.isHttp2; @@ -186,6 +187,9 @@ if (name === 'isCustomSSL') { isCustomSSL = !isCustomSSL; } + if (name === 'basicAuth') { + basicAuth = !basicAuth; + } if (name === 'isBot') { if ($status.application.overallStatus !== 'stopped') return; isBot = !isBot; @@ -210,7 +214,10 @@ isCustomSSL, isHttp2, branch: application.branch, - projectId: application.projectId + projectId: application.projectId, + basicAuth, + basicAuthUser: application.basicAuthUser, + basicAuthPw: application.basicAuthPw }); return addToast({ message: $t('application.settings_saved'), @@ -232,6 +239,9 @@ if (name === 'isBot') { isBot = !isBot; } + if (name === 'basicAuth') { + basicAuth = !basicAuth; + } if (name === 'isDBBranching') { isDBBranching = !isDBBranching; } @@ -498,7 +508,7 @@
General
{#if $appSession.isAdmin}