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')