~ekez/negativefour

2fdc4752df98f2e102c6a7d51a49b123dd322b35 — Zeke Medley 8 months ago 3609f28
Add option to undeploy webpages from the webpage
M docs/serve-api.md => docs/serve-api.md +2 -1
@@ 45,13 45,14 @@ field and the HTTP status code will be set appropriately.
}
```

## `/undeploy`
## `/delete`

POST JSON body params:

```
{
	name: <name of the webpage>,
	user: <user email that controls name>
}
```


M serve/app.js => serve/app.js +7 -0
@@ 183,6 183,13 @@ app.post('/delete', requireAuth, function(req, res) {

    child.on('exit', code => {
	jobStatusMap.get(childProcessID).exitCode = code
	if (code !== 0) {
	    sendDiscordAlert(`undeploy of (${name}) for (${user}) failed with exit code (${code})`)
	    console.log(jobStatusMap.get(childProcessID))
	} else {
	    const clearnet = `https://${name}.negativefour.app`
	    await db.registerUndeploy(user, name, clearnet)
	}
    })

    child.on('error', err => {

M serve/helpers/db.js => serve/helpers/db.js +7 -0
@@ 36,3 36,10 @@ module.exports.registerDeploy = async (useremail, name, clearnet, darknet) => {
    const client = await getClient()
    await client.query(`INSERT INTO deployments (useremail, name, clearnet, darknet) VALUES ('${useremail}', '${name}', '${clearnet}', '${darknet}')`)
}

// Webpages are uniquely identified by their name and the controling
// user.
module.exports.registerUndeploy = async (userEmail, name) => {
    const client = await getClient()
    await client.query(`DELETE FROM deployments WHERE useremail='${useremail}' AND name='${name}'`)
}

M www/app.js => www/app.js +7 -1
@@ 8,6 8,9 @@ require('dotenv').config()

const indexRouter = require('./routes/index');
const deployRouter = require('./routes/deploy.js')
const viewRouter = require('./routes/view.js')
const statusRouter = require('./routes/status.js')
const undeployRouter = require('./routes/undeploy.js')

var app = express();



@@ 21,8 24,11 @@ app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/', indexRouter)
app.use('/deploy', deployRouter)
app.use('/view', viewRouter)
app.use('/status', statusRouter)
app.use('/undeploy', undeployRouter)

// catch 404 and forward to error handler
app.use(function(req, res, next) {

M www/helpers/db.js => www/helpers/db.js +6 -0
@@ 54,3 54,9 @@ module.exports.getUserDeployments = async (email) => {
    const res = await client.query(`SELECT * FROM deployments WHERE useremail='${email}'`)
    return res.rows
}

module.exports.getUserDeployment = async (email, name) => {
    const client = await getClient()
    const res = await client.query(`SELECT * FROM deployments WHERE useremail='${email}' AND name='${name}'`)
    return res.rows
}

M www/public/stylesheets/style.css => www/public/stylesheets/style.css +20 -6
@@ 64,23 64,28 @@ input[type=submit] {
    background-color: #303134;
    color: #e8eaed;

    padding: 8px 16px;
    transition: box-shadow 0.2s ease;
}

input[type=submit] {
    padding: 8px 16px;
input[type=checkbox] {
    width: auto;
    margin: 0.4rem 0.8rem;
}

.checkbox-item {
    display: flex;
    border: 1px solid #e0325a;
    border-radius: 6px;
    background-color: rgba(224, 50, 90, 0.25);
}

.webpage-display {
    padding: 16px 32px;
    border-radius: 6px;

    background-color: #303134;

    min-width: 131px;

    transition: box-shadow 0.2s ease;

    align-items: stretch;
}



@@ 108,6 113,15 @@ input[type=submit]:active, webpage-display:active {
    gap: 32px;
}

#delete-summary {
    font-size: 20px;
    margin: 0.8rem 0.4rem;
}

details {
    border-radius: 6px;
}

footer {
    margin-top: 40px;
}

M www/routes/deploy.js => www/routes/deploy.js +6 -43
@@ 39,51 39,14 @@ router.post('/', requireAuth, async function(req, res, next) {
	    token: signedData
	})

	return res.redirect(`/deploy/status/${deployRes.data.jobID}`)
	return res.redirect(`/status/${deployRes.data.jobID}/${name}/deploy`)
    } catch (err) {
	return res.status(500).send(err)
    }
})

router.get('/status/:jobID', requireAuth, async function(req, res, next) {
    const jobID = req.params.jobID

    const signedData = jwt.sign({
	jobID: jobID
    }, process.env.JWT_INTERNAL_KEY)

    try {
	const get = await axios.get('https://serve.negativefour.com/status', {
	    data: { token: signedData }
	})
	const status = get.data

	if (status.exitCode !== undefined) {
	    const code = status.exitCode
	    if (code === 0) {
		return res.status(200).render('status-success', {
		    status: status,
		    userEmail: req.useremail
		})
	    } else {
		return res.status(500).render('status-fail', {
		    userEmail: req.useremail,
		    status: status
		})
	    }
	}

	return res.status(200).render('status', {
	    status: status,
	    userEmail: req.useremail
	})
    } catch (err) {
	return res.status(500).render('error', {
	    message: '500 - Internal Server Error',
	res.status(500).render('error', {
	    message: '500 - Server error communicating with serve api',
	    userEmail: req.useremail,
	    error: {
		status: 'An error occured getting the build status. Please send Zeke an email if this persists.',
		stack: err,
		userEmail: req.useremail
		status: `An error occured while processing a deploy action for user (${user}) and webpage (${name})`,
		stack: req.url,
	    }
	})
    }

M www/routes/index.js => www/routes/index.js +1 -1
@@ 39,7 39,7 @@ const tokenMap = new KV()

router.get('/', requireAuth, async function(req, res, next) {
    const deployments = await db.getUserDeployments(req.useremail)
    res.render('index', { userEmail: req.username, userWebpages: deployments });
    res.render('index', { userEmail: req.useremail, userWebpages: deployments });
});

// The initial login page where users present their emails.

A www/routes/status.js => www/routes/status.js +96 -0
@@ 0,0 1,96 @@
// Copyright (C) 2021  Zeke Medley
//
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU Affero General Public License
// as published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public
// License along with this program.  If not, see
// <https://www.gnu.org/licenses/>.

// Logic related to checking the status of a deploy or delete
// operation occuring via the serve.negativefour.com api.

const express = require('express')
const jwt = require('jsonwebtoken')
const axios = require('axios')

const requireAuth = require('../helpers/require-auth.js')
const db = require('../helpers/db.js')

const router = express.Router()

router.get('/:jobID/:name/:type', requireAuth, async function(req, res, next) {
    const jobID = req.params.jobID
    const name = req.params.name

    // Type is either deploy or undeploy
    const type = req.params.type

    const signedData = jwt.sign({
	jobID: jobID
    }, process.env.JWT_INTERNAL_KEY)

    try {
	const get = await axios.get('https://serve.negativefour.com/status', {
	    data: { token: signedData }
	})
	const status = get.data

	if (status.exitCode !== undefined) {
	    const code = status.exitCode
	    if (code === 0) {
		const deployments = await db.getUserDeployment(req.useremail, name)

		if (deployments.length !== 1) {
		    return res.status(404).render('error', {
			message: `404 - there is no deployment with name (${name})`,
			userEmail: req.useremail,
			error: {
			    status: 'If you got here from the homepage this is likely an internal error. Please send me an email if you think something is wrong.',
			    stack: `${req.url}

${deployments}`,
			}
		    })
		}

		const deployment = deployments[0]

		return res.status(200).render('status-success', {
		    status: status,
		    userEmail: req.useremail,
		    clearnet: deployment.clearnet,
		    type: type,
		})
	    } else {
		return res.status(500).render('status-fail', {
		    userEmail: req.useremail,
		    status: status
		})
	    }
	}

	return res.status(200).render('status', {
	    status: status,
	    userEmail: req.useremail
	})
    } catch (err) {
	return res.status(500).render('error', {
	    message: '500 - Internal Server Error',
	    error: {
		status: 'An error occured getting the build status. Please send Zeke an email if this persists.',
		stack: err,
		userEmail: req.useremail
	    }
	})
    }
})

module.exports = router

A www/routes/undeploy.js => www/routes/undeploy.js +36 -0
@@ 0,0 1,36 @@
const express = require('express')
const https = require('https')
const jwt = require('jsonwebtoken')
const axios = require('axios')

const requireAuth = require('../helpers/require-auth.js')

const router = express.Router()

router.post('/:name', requireAuth, async function(req, res, next) {
    const name = req.params.name

    const signedData = jwt.sign({
	name: name,
	user: req.useremail,
    }, process.env.JWT_INTERNAL_KEY)

    try {
	const deployRes = await axios.post('https://serve.negativefour.com/delete', {
	    token: signedData
	})

	return res.redirect(`/status/${deployRes.data.jobID}/${name}/delete`)
    } catch (err) {
	res.status(500).render('error', {
	    message: '500 - Server error communicating with serve api',
	    userEmail: req.useremail,
	    error: {
		status: 'An error occured while processing a delete action',
		stack: req.url,
	    }
	})
    }
})

module.exports = router

A www/routes/view.js => www/routes/view.js +49 -0
@@ 0,0 1,49 @@
// Copyright (C) 2021  Zeke Medley
//
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU Affero General Public License
// as published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public
// License along with this program.  If not, see
// <https://www.gnu.org/licenses/>.

const express = require('express')

const requireAuth = require('../helpers/require-auth.js')
const db = require('../helpers/db.js')

const router = express.Router()

router.get('/:name', requireAuth, async function(req, res, next) {
    const name = req.params.name
    const deployments = await db.getUserDeployment(req.useremail, name)

    if (deployments.length !== 1) {
	return res.status(404).render('error', {
	    message: `404 - there is no deployment with name (${name})`,
	    userEmail: req.useremail,
	    error: {
		status: 'If you got here from the homepage this is likely an internal error. Please send me an email if you think something is wrong.',
		stack: `${req.url}

${deployments}`,
	    }
	})
    }

    const deployment = deployments[0]

    return res.status(200).render('view-deployment', {
	userEmail: req.useremail,
	page: deployment,``
    })
})

module.exports = router

M www/views/error.pug => www/views/error.pug +1 -1
@@ 2,5 2,5 @@ extends layout

block content
  h1= message
  h2= error.status
  h3= error.status
  pre #{error.stack}

M www/views/index.pug => www/views/index.pug +1 -1
@@ 9,7 9,7 @@ block content
  div(id='webpages')
    each page in userWebpages
      div(class='webpage-display')
        h3= page.name
        h3 #[a(href=`/view/${page.name}`) #{page.name}]
        a(href=`${page.clearnet}` target='_blank')
          img(src='/images/world-wide-web.svg')
        a(href=`${page.darknet}` target='_blank')

M www/views/status-success.pug => www/views/status-success.pug +4 -1
@@ 2,7 2,10 @@ extends layout

block content
  h1 Success! 🎊
  p Your webpage is now avaliable at #[a(href='/') webpage.negativefour.app]
  if type === "deploy"
    p Your webpage is now avaliable at #[a(href=`${clearnet}`) #{clearnet}]
  else
    p #{clearnet} was deleted successfully
  if status.stdout
    h2 stdout
    pre(class='stdout') #{status.stdout}

A www/views/view-deployment.pug => www/views/view-deployment.pug +17 -0
@@ 0,0 1,17 @@
extends layout

block content
  br
  div(class='webpage-display' style='max-width: 500px; box-sizing: border-box;')
    h3= page.name
    a(href=`${page.clearnet}` target='_blank')
      img(src='/images/world-wide-web.svg')
    a(href=`${page.darknet}` target='_blank')
      img(src='/images/tor.svg')
  details
    summary(id='delete-summary') Delete this webpage
    form(action=`/undeploy/${page.name}` method='post')
      div(class='checkbox-item')
        input(type='checkbox' name='yes-really' required)
        label(for='yes-really') I realy would like to delete "#{page.name}" and understand that this will entirely remove this webpage from the internet.
      input(type='submit' value='Delete')