~ekez/negativefour

9000067f2ad7968a0aee1b41814d3149d20508e1 — Zeke Medley 8 months ago 2d8c12d
Add ability to deploy webpages from ui
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')