M serve/app.js => serve/app.js +5 -1
@@ 32,7 32,7 @@ app.use(express.json())
const requireAuth = (req, res, next) => {
const signedToken = req.body.token
if (!signedToken) {
- res.status(400).json({
+ return res.status(400).json({
error: 'Invalid request - no token.'
})
}
@@ 94,6 94,10 @@ app.post('/deploy', requireAuth, function(req, res) {
child.on('exit', code => {
jobStatusMap.get(childProcessID).exitCode = code
+ if (code !== 0) {
+ sendDiscordAlert(`deploy of (${name}) for (${user}) failed with exit code (${code})`)
+ console.log(jobStatusMap.get(childProcessID))
+ }
})
child.on('error', err => {
M www/app.js => www/app.js +3 -1
@@ 6,7 6,8 @@ var logger = require('morgan');
require('dotenv').config()
-var indexRouter = require('./routes/index');
+const indexRouter = require('./routes/index');
+const deployRouter = require('./routes/deploy.js')
var app = express();
@@ 21,6 22,7 @@ app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
+app.use('/deploy', deployRouter)
// catch 404 and forward to error handler
app.use(function(req, res, next) {
M www/package-lock.json => www/package-lock.json +41 -0
@@ 8,6 8,7 @@
"name": "www",
"version": "0.0.0",
"dependencies": {
+ "axios": "^0.21.4",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"dotenv": "^10.0.0",
@@ 89,6 90,14 @@
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
},
+ "node_modules/axios": {
+ "version": "0.21.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
+ "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "dependencies": {
+ "follow-redirects": "^1.14.0"
+ }
+ },
"node_modules/babel-walk": {
"version": "3.0.0-canary-5",
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
@@ 347,6 356,25 @@
"node": ">= 0.8"
}
},
+ "node_modules/follow-redirects": {
+ "version": "1.14.4",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
+ "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ 1089,6 1117,14 @@
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
},
+ "axios": {
+ "version": "0.21.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
+ "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "requires": {
+ "follow-redirects": "^1.14.0"
+ }
+ },
"babel-walk": {
"version": "3.0.0-canary-5",
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
@@ 1301,6 1337,11 @@
"unpipe": "~1.0.0"
}
},
+ "follow-redirects": {
+ "version": "1.14.4",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
+ "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
+ },
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
M www/package.json => www/package.json +3 -1
@@ 3,9 3,11 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "start": "node ./bin/www"
+ "start": "node ./bin/www",
+ "live": "nodemon ./bin/www"
},
"dependencies": {
+ "axios": "^0.21.4",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"dotenv": "^10.0.0",
M www/public/stylesheets/style.css => www/public/stylesheets/style.css +49 -27
@@ 1,4 1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap');
body {
font-family: "Lato", sans-serif;
@@ 12,7 12,7 @@ nav {
display: flex;
border-bottom: 1px solid black;
margin-top: 24px;
- padding: 8px;
+ padding: 8px 0px;
justify-content: space-between;
}
@@ 20,18 20,15 @@ nav p {
margin: 0;
}
+#welcome {
+ margin-top: 32px;
+}
+
#userinfo {
display: flex;
gap: 20px;
}
-
-#deploy-button {
- display: inline-block;
- margin-top: 16px;
- margin-bottom: 16px;
-}
-
form {
max-width: 500px;
}
@@ 48,9 45,7 @@ input {
width: 100%;
}
-input[type=submit], button {
- /* font-size: 18px; */
- /* font-weight: bold; */
+input[type=submit] {
font-weight: 400;
border-radius: 6px;
@@ 60,27 55,25 @@ input[type=submit], button {
background-color: black;
color: white;
- box-shadow: 0 2px 4px rgba(44,39,56,.2),0 4px 8px rgba(44,39,56,.2);
+ box-shadow: 0 1px 4px rgba(44,39,56,.2),0 4px 8px rgba(44,39,56,.2);
transition: box-shadow 0.2s ease;
}
-button {
- padding: 16px 32px;
-}
-
input[type=submit] {
padding: 8px 16px;
}
.webpage-display {
- border: 1px solid black;
padding: 16px 32px;
border-radius: 6px;
margin-bottom: 32px;
- min-width: 131.1px;;
- box-shadow: 0 2px 4px rgba(44,39,56,.2),0 4px 8px rgba(44,39,56,.2);
+ min-width: 131px;
+
+ box-shadow: 0 1px 4px rgba(44,39,56,.2),0 4px 8px rgba(44,39,56,.2);
transition: box-shadow 0.2s ease;
+
+ align-items: stretch;
}
.webpage-display > h3 {
@@ 88,16 81,11 @@ input[type=submit] {
margin-top: 0.25rem;
}
-.webpage-display > a {
- color: grey;
- line-height: 1.5;
-}
-
-button:hover, .webpage-display:hover, input[type=submit]:hover {
+.webpage-display:hover, input[type=submit]:hover {
box-shadow: 0 2px 4px rgba(44,39,56,.3),0 4px 8px rgba(44,39,56,.3);
}
-input[type=submit]:active, button:active, webpage-display:active {
+input[type=submit]:active, webpage-display:active {
box-shadow: 0 2px 4px rgba(44,39,56,.1),0 4px 8px rgba(44,39,56,.1);
}
@@ 127,3 115,37 @@ footer ul {
padding-left: 0;
gap: 24px;
}
+
+.stdout, .stderr {
+ border-radius: 8px;
+ padding: 8px;
+ overflow: scroll;
+}
+
+.stdout {
+ box-shadow: 0 1px 4px rgba(44,39,255,.2),0 4px 8px rgba(44,39,255,.2);
+}
+
+.stderr {
+ box-shadow: 0 1px 4px rgba(255,39,56,.2),0 4px 8px rgba(255,39,56,.2);
+}
+
+.plus {
+ display:inline-block;
+ width:50px;
+ height:50px;
+ background:
+ linear-gradient(#000,#000),
+ linear-gradient(#000,#000);
+ background-position:center;
+ background-size: 50% 2px,2px 50%; /*thickness = 2px, length = 50% (25px)*/
+ background-repeat:no-repeat;
+}
+
+.new-webpage {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ max-width: 65.5px;
+}
A www/routes/deploy.js => www/routes/deploy.js +92 -0
@@ 0,0 1,92 @@
+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.get('/', requireAuth, function(req, res, next) {
+ res.render('deploy', { userEmail: req.userEmail } )
+})
+
+router.post('/', requireAuth, async function(req, res, next) {
+ const name = req.body.name
+ const repo = req.body.repo
+ const user = req.userEmail
+
+ if (!name || !repo || !user) {
+ return res.status(400).render('error', {
+ message: '400 - Invalid Request',
+ userEmail: req.userEmail,
+ error: {
+ status: 'The server doesn\'t know what to do.',
+ stack: req.url,
+ }})
+ }
+
+ // Send post request to initiate a deploy. `../../serve/app.js`
+ // will handle this request.
+ const signedData = jwt.sign({
+ name: name,
+ repo: repo,
+ user: user,
+ }, process.env.JWT_INTERNAL_KEY)
+
+ try {
+ const deployRes = await axios.post('https://serve.negativefour.com/deploy', {
+ token: signedData
+ })
+
+ return res.redirect(`/deploy/status/${deployRes.data.jobID}`)
+ } 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',
+ 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
M www/views/deploy.pug => www/views/deploy.pug +4 -4
@@ 4,9 4,9 @@ block content
h1 Deploy a webpage
form(action='/deploy' method='post')
div(class='form-item')
- label(for='clone-url') The git repo containing your static website.
- input(type='text' name='clone-url' placeholder='https://git.sr.ht/~ekez/cat.negativefour.app' required)
+ label(for='repo') The git repo containing your static website.
+ input(type='text' name='repo' placeholder='https://git.sr.ht/~ekez/cat.negativefour.app' required)
div(class='form-item')
- label(for='subdomain') The negativefour.app subdomain that your website will be deployed to. For example, selecting cat would cause your webpage to be deployed at #[a(href="https://cat.negativefour.app") cat.negativefour.app].
- input(type='text' name='subdomain' placeholder='fish' required)
+ label(for='name') The negativefour.app subdomain that your website will be deployed to. For example, selecting cat would cause your webpage to be deployed at #[a(href="https://cat.negativefour.app") cat.negativefour.app].
+ input(type='text' name='name' placeholder='cat' required)
input(type='submit' value='Deploy')
M www/views/index.pug => www/views/index.pug +13 -14
@@ 1,17 1,16 @@
extends layout
block content
- h1 negativefour
- p Welcome back, #[i #{userEmail}]!
- a(href='/deploy' id='deploy-button')
- button Deploy a webpage
- if userWebpages
- h2 Your webpages
- div(id='webpages')
- each page in userWebpages
- div(class='webpage-display')
- h3= page.name
- a(href=`${page.clearnet}` target='_blank')
- img(src='/images/world-wide-web.svg')
- a(href=`${page.union}` target='_blank')
- img(src='/images/tor.svg')
+ h1 Welcome!
+ h2 Deploy a webpage
+ a(href='/deploy' class='new-webpage webpage-display')
+ div(class='plus')
+ h2 Manage your webpages
+ div(id='webpages')
+ each page in userWebpages
+ div(class='webpage-display')
+ h3= page.name
+ a(href=`${page.clearnet}` target='_blank')
+ img(src='/images/world-wide-web.svg')
+ a(href=`${page.union}` target='_blank')
+ img(src='/images/tor.svg')
M www/views/layout.pug => www/views/layout.pug +1 -0
@@ 4,6 4,7 @@ html
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
meta(name='viewport' content='width=device-width, initial-scale=1')
+ block extraHeaders
body
nav
a(href='/') home
A www/views/status-fail.pug => www/views/status-fail.pug +13 -0
@@ 0,0 1,13 @@
+extends layout
+
+block content
+ h1 Deploy Failure 😔
+ p If the logs below don't explain why please send me an email via the contact link in the footer.
+ if status.stdout
+ h2 stdout
+ pre(class='stdout') #{status.stdout}
+ if status.stderr
+ h2 stderr
+ pre(class='stderr')
+ | #{status.stderr}
+ | process exited with code (#{status.exitCode})
A www/views/status-success.pug => www/views/status-success.pug +11 -0
@@ 0,0 1,11 @@
+extends layout
+
+block content
+ h1 Success! 🎊
+ p Your webpage is now avaliable at #[a(href='/') webpage.negativefour.app]
+ if status.stdout
+ h2 stdout
+ pre(class='stdout') #{status.stdout}
+ if status.stderr
+ h2 stderr
+ pre(class='stderr') #{status.stderr}
A www/views/status.pug => www/views/status.pug +14 -0
@@ 0,0 1,14 @@
+extends layout
+
+block content
+ h1 Build Status
+ h2 ⏳
+ if status.stdout
+ h2 stdout
+ pre(class='stdout') #{status.stdout}
+ if status.stderr
+ h2 stderr
+ pre(class='stderr') #{status.stderr}
+
+block extraHeaders
+ meta(http-equiv='refresh' content='2')