diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b3c99..d1206e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Display expiration date of SSL certificates (commit [48218](https://github.com/MathieuRA/nguix/commit/4821882d17b8cb7d23fdcdf9452b0d48e2884244)) - Display `nginx.service` status(commit [852bd](https://github.com/MathieuRA/nguix/commit/852bd52e735687fa4b6855f3f41bba0eae322e2f)) - Disable/Enable virtual hosts (commit [af4f6](https://github.com/MathieuRA/nguix/commit/af4f6578b05386e9ffab3aeb8a9a1d91ad0b10ed)) +- Destroy virtual host (PR [#4](https://github.com/MathieuRA/nguix/pull/4)) ## Fixes diff --git a/back/src/classes/router.ts b/back/src/classes/router.ts index 3ae3bed..41c2e7f 100644 --- a/back/src/classes/router.ts +++ b/back/src/classes/router.ts @@ -4,13 +4,13 @@ import { serveStatic } from '../static.ts' // @ts-ignore type HttpCallback = (params: { request: http.IncomingMessage, response: http.ServerResponse, [key: string]: number | string, body: (req: http.IncomingMessage) => Promise }) => void | ApiResponse | Promise -type HttpVerb = 'get' | 'patch' +type HttpVerb = 'get' | 'delete' | 'patch' type RouteConfig = { path: string callback: HttpCallback method: HttpVerb, } -const HTTP_VERB: HttpVerb[] = ['get', 'patch'] +const HTTP_VERB: HttpVerb[] = ['get', 'delete', 'patch'] const BASE_PATH = '/api' @@ -87,6 +87,10 @@ export class Router { this.#addRoute({ method: 'get', callback, path }) } + delete(path: string, callback: HttpCallback) { + this.#addRoute({ method: 'delete', callback, path }) + } + patch(path: string, callback: HttpCallback) { this.#addRoute({ method: 'patch', callback, path }) } diff --git a/back/src/classes/vhost.ts b/back/src/classes/vhost.ts index 1fc9fb1..b361d79 100644 --- a/back/src/classes/vhost.ts +++ b/back/src/classes/vhost.ts @@ -39,7 +39,8 @@ export class Vhost { return { domain, aliases, port, ssl, sslExpiry, proxy }; } - private readonly _filePath: string + /** null if the sym link doesn't exist anymore. (e.g. after a disable call) */ + private _filePath: string | null private readonly _symlink: boolean private readonly _fileName: string private readonly _enabled: boolean @@ -47,7 +48,7 @@ export class Vhost { private readonly _id: string constructor(params: { path: string, symlink: boolean, conf: VhostConf }) { - this._filePath = Object.freeze(params.path) + this._filePath = params.path this._symlink = Object.freeze(params.symlink) this._fileName = Object.freeze(params.path.split('/').pop() ?? '_unknown_') this._enabled = Object.freeze(this._symlink) @@ -83,6 +84,13 @@ export class Vhost { } linkTo() { + if (!this.isSymlink) { + throw new Error('not a symlink') + } + if (this.filePath === null) { + throw new Error('not an active symlink') + } + return fs.readlink(this.filePath) } @@ -118,13 +126,33 @@ export class Vhost { throw new Error("update partially applied") } + async destroy() { + const path = this.isSymlink ? await this.linkTo() : this.filePath! + + if (this.enabled) { + await this.#disable() + } + + await fs.rm(path) + } + async #enable() { - await fs.symlink(this.filePath, `${getEnv('SITES_ENABLED')}/${this.fileName}`, 'file') + if (this.isSymlink) { + throw new Error('[enable vhost]: cannot enable a symlink') + } + + await fs.symlink(this.filePath!, `${getEnv('SITES_ENABLED')}/${this.fileName}`, 'file') } async #disable() { if (this.isSymlink) { + if (this.filePath === null) { + console.warn('[disable vhost]: vhost already disabled') + return + } + await fs.unlink(this.filePath) + this._filePath = null } else { console.error('trying to unlink a non symlink', this) } diff --git a/back/src/routes/index.ts b/back/src/routes/index.ts index e467170..dc51758 100644 --- a/back/src/routes/index.ts +++ b/back/src/routes/index.ts @@ -11,6 +11,13 @@ router.get('/vhosts', async () => { return new ApiResponse({ data: vHosts.map(host => host.toJSON()) }) }) +router.delete('/vhosts/:id', async ({ id }) => { + const vhost = await getVirtualHost(decodeURIComponent(id as string)) + await vhost.destroy() + + return new ApiResponse({ status: 204 }) +}) + router.patch('/vhosts/:id', async ({ id, request, body }) => { const vhost = await getVirtualHost(decodeURIComponent(id as string)) diff --git a/front/app.js b/front/app.js index a9c43ad..d1dadc4 100644 --- a/front/app.js +++ b/front/app.js @@ -11,7 +11,7 @@ function setFilter(el) { renderVhosts(); } -async function getVhosts(opts) { +async function getVhosts(opts = {}) { // TODO: handle cache expiration if (CACHE['vhosts'] === undefined || opts.bypassCache) { const response = await fetch('/vhosts') @@ -37,6 +37,17 @@ async function toggleVhost(id, value) { await renderVhosts({ bypassCache: true }) } +async function destroyVhost(id) { + const vhost = (await getVhosts()).find(vhost => vhost.id === id) + confirm(`Are you sure you want to destroy the vhost: ${vhost.conf.domain}`) + + await fetch(`/vhosts/${encodeURIComponent(id)}`, { + method: 'DELETE' + }) + + await renderVhosts({ bypassCache: true }) +} + async function renderVhosts(opts) { const query = document.getElementById('search').value.toLowerCase(); @@ -86,10 +97,9 @@ async function renderVhosts(opts) { + '' + '' + '' - + '' + ''