~ekez/negativefour

88309722ec10f68e007b0491432446c7843a5d5d — Zeke Medley 1 year, 11 months ago 2ddbcfe
Store deployment and user information in database
A docs/db.md => docs/db.md +45 -0
@@ 0,0 1,45 @@
## Database Configuration

negativefour is backed by a postgres database.

### Creating a new database

On the machine running the database:

```
; sudo su postgres
; CREATE DATABASE negativefour_test WITH OWNER admin;
```

This creates a new databse called negativefour_test which can be
accessed by the admin user. The "admin" user matches the name of the
non root user which runs the application.

### Test and prod databses

`negativefour_test` is a testing databse. It accepts external
connections with a password so it is suitable for "local" development
use as there is only one of me.

`negativefour` is the real database. It does not accept external
connections and can only be accessed from inside production
infrastructure.

### Manually inspecting the database

The test database is accessible from anywhere with the following
command:

```
psql -h testdb.negativefour.com -p 5432 -d negativefour_test -U admin -W
```

This will prompt for a password and the open an interactive postgres
console. The password is stored in our password manager.

The production database can only be accessed from the machine serving
webpages and can be accessed by running

```
psql negativefour
```

M serve/app.js => serve/app.js +9 -1
@@ 15,12 15,15 @@
// <https://www.gnu.org/licenses/>.

const express = require('express')
const fs = require('fs')
const jwt = require('jsonwebtoken')
const { execFile } = require('child_process')
const axios = require('axios')

require('dotenv').config()

const db = require('helpers/db.js')

const app = express()
const port = 2998;



@@ 92,11 95,16 @@ app.post('/deploy', requireAuth, function(req, res) {
	jobStatusMap.get(childProcessID).stderr += data
    })

    child.on('exit', code => {
    child.on('exit', async 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))
	} else {
	    const clearnet = `https://${name}.negativefour.app`
	    const darknet = 'http://' + (await fs.readFile(`/var/lib/tor/${name}.negativefour.app/hostname`)).toString()

	    await db.registerDeploy(user, name, clearnet, darknet)
	}
    })


A serve/helpers/db.js => serve/helpers/db.js +38 -0
@@ 0,0 1,38 @@
// 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 { Client } = require('pg')

var theClient = null;

const getClient = async () => {
    if (theClient === null) {
	theClient = new Client({
	    user: process.env.DB_USER,
	    host: process.env.DB_HOST,
	    database: process.env.DB_NAME,
	    password: process.env.DB_PASS,
	    port: process.env.DB_PORT,
	})
	await theClient.connect()
    }
    return theClient
}

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

A www/helpers/db.js => www/helpers/db.js +56 -0
@@ 0,0 1,56 @@
// 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 { Client } = require('pg')

var theClient = null;

const getClient = async () => {
    if (theClient === null) {
	theClient = new Client({
	    user: process.env.DB_USER,
	    host: process.env.DB_HOST,
	    database: process.env.DB_NAME,
	    password: process.env.DB_PASS,
	    port: process.env.DB_PORT,
	})
	await theClient.connect()
    }
    return theClient
}

module.exports.getNow = async () => {
    const client = await getClient()
    return await client.query('SELECT NOW()')
}

module.exports.emailRegistered = async (email) => {
    const client = await getClient()
    const res = await client.query(`SELECT 1 FROM users WHERE email='${email}'`)
    return res.rows.length === 1;
}

module.exports.getNameFromEmail = async (email) => {
    const client = await getClient()
    const res = await client.query(`SELECT username FROM users WHERE email='${email}'`)
    return res.rows[0].username
}

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

M www/helpers/require-auth.js => www/helpers/require-auth.js +2 -1
@@ 23,7 23,8 @@ const requireAuth = (req, res, next) => {
    }
    try {
	const decoded = jwt.verify(signedToken, process.env.JWT_KEY)
	req.userEmail = decoded.email
	req.useremail = decoded.email
	req.username = decoded.username
    } catch (err) {
	return res.status(401).render('error', { message: 'Invalid authentication token.' })
    }

M www/package-lock.json => www/package-lock.json +315 -0
@@ 17,6 17,7 @@
        "jsonwebtoken": "^8.5.1",
        "morgan": "~1.9.1",
        "nodemailer": "^6.6.3",
        "pg": "^8.7.1",
        "pug": "^3.0.2",
        "uuid": "^8.3.2"
      }


@@ 145,6 146,14 @@
      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
      "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
    },
    "node_modules/buffer-writer": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
      "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/bytes": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",


@@ 728,6 737,11 @@
        "node": ">= 0.8"
      }
    },
    "node_modules/packet-reader": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
      "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",


@@ 746,6 760,115 @@
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
    },
    "node_modules/pg": {
      "version": "8.7.1",
      "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz",
      "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==",
      "dependencies": {
        "buffer-writer": "2.0.0",
        "packet-reader": "1.0.0",
        "pg-connection-string": "^2.5.0",
        "pg-pool": "^3.4.1",
        "pg-protocol": "^1.5.0",
        "pg-types": "^2.1.0",
        "pgpass": "1.x"
      },
      "engines": {
        "node": ">= 8.0.0"
      },
      "peerDependencies": {
        "pg-native": ">=2.0.0"
      },
      "peerDependenciesMeta": {
        "pg-native": {
          "optional": true
        }
      }
    },
    "node_modules/pg-connection-string": {
      "version": "2.5.0",
      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
      "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
    },
    "node_modules/pg-int8": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
      "engines": {
        "node": ">=4.0.0"
      }
    },
    "node_modules/pg-pool": {
      "version": "3.4.1",
      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz",
      "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==",
      "peerDependencies": {
        "pg": ">=8.0"
      }
    },
    "node_modules/pg-protocol": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
      "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
    },
    "node_modules/pg-types": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
      "dependencies": {
        "pg-int8": "1.0.1",
        "postgres-array": "~2.0.0",
        "postgres-bytea": "~1.0.0",
        "postgres-date": "~1.0.4",
        "postgres-interval": "^1.1.0"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/pgpass": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz",
      "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==",
      "dependencies": {
        "split2": "^3.1.1"
      }
    },
    "node_modules/postgres-array": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/postgres-bytea": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
      "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/postgres-date": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/postgres-interval": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
      "dependencies": {
        "xtend": "^4.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/promise": {
      "version": "7.3.1",
      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",


@@ 908,6 1031,19 @@
        "node": ">= 0.8"
      }
    },
    "node_modules/readable-stream": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
      "dependencies": {
        "inherits": "^2.0.3",
        "string_decoder": "^1.1.1",
        "util-deprecate": "^1.0.1"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/resolve": {
      "version": "1.20.0",
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",


@@ 980,6 1116,14 @@
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
    },
    "node_modules/split2": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
      "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
      "dependencies": {
        "readable-stream": "^3.0.0"
      }
    },
    "node_modules/statuses": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",


@@ 988,6 1132,33 @@
        "node": ">= 0.6"
      }
    },
    "node_modules/string_decoder": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
      "dependencies": {
        "safe-buffer": "~5.2.0"
      }
    },
    "node_modules/string_decoder/node_modules/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ]
    },
    "node_modules/to-fast-properties": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",


@@ 1021,6 1192,11 @@
        "node": ">= 0.8"
      }
    },
    "node_modules/util-deprecate": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
    },
    "node_modules/utils-merge": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",


@@ 1066,6 1242,14 @@
      "engines": {
        "node": ">= 10.0.0"
      }
    },
    "node_modules/xtend": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
      "engines": {
        "node": ">=0.4"
      }
    }
  },
  "dependencies": {


@@ 1163,6 1347,11 @@
      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
      "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
    },
    "buffer-writer": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
      "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
    },
    "bytes": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",


@@ 1618,6 1807,11 @@
      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
    },
    "packet-reader": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
      "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
    },
    "parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",


@@ 1633,6 1827,84 @@
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
    },
    "pg": {
      "version": "8.7.1",
      "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz",
      "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==",
      "requires": {
        "buffer-writer": "2.0.0",
        "packet-reader": "1.0.0",
        "pg-connection-string": "^2.5.0",
        "pg-pool": "^3.4.1",
        "pg-protocol": "^1.5.0",
        "pg-types": "^2.1.0",
        "pgpass": "1.x"
      }
    },
    "pg-connection-string": {
      "version": "2.5.0",
      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
      "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
    },
    "pg-int8": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
    },
    "pg-pool": {
      "version": "3.4.1",
      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz",
      "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==",
      "requires": {}
    },
    "pg-protocol": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
      "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
    },
    "pg-types": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
      "requires": {
        "pg-int8": "1.0.1",
        "postgres-array": "~2.0.0",
        "postgres-bytea": "~1.0.0",
        "postgres-date": "~1.0.4",
        "postgres-interval": "^1.1.0"
      }
    },
    "pgpass": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz",
      "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==",
      "requires": {
        "split2": "^3.1.1"
      }
    },
    "postgres-array": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
    },
    "postgres-bytea": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
      "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU="
    },
    "postgres-date": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
    },
    "postgres-interval": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
      "requires": {
        "xtend": "^4.0.0"
      }
    },
    "promise": {
      "version": "7.3.1",
      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",


@@ 1783,6 2055,16 @@
        "unpipe": "1.0.0"
      }
    },
    "readable-stream": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
      "requires": {
        "inherits": "^2.0.3",
        "string_decoder": "^1.1.1",
        "util-deprecate": "^1.0.1"
      }
    },
    "resolve": {
      "version": "1.20.0",
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",


@@ 1843,11 2125,34 @@
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
    },
    "split2": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
      "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
      "requires": {
        "readable-stream": "^3.0.0"
      }
    },
    "statuses": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
      "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
    },
    "string_decoder": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
      "requires": {
        "safe-buffer": "~5.2.0"
      },
      "dependencies": {
        "safe-buffer": {
          "version": "5.2.1",
          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
          "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
        }
      }
    },
    "to-fast-properties": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",


@@ 1872,6 2177,11 @@
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
    },
    "util-deprecate": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
    },
    "utils-merge": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",


@@ 1902,6 2212,11 @@
        "assert-never": "^1.2.1",
        "babel-walk": "3.0.0-canary-5"
      }
    },
    "xtend": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
    }
  }
}

M www/package.json => www/package.json +3 -2
@@ 3,8 3,8 @@
  "version": "0.0.0",
  "private": true,
  "scripts": {
      "start": "node ./bin/www",
      "live": "nodemon ./bin/www"
    "start": "node ./bin/www",
    "live": "nodemon ./bin/www"
  },
  "dependencies": {
    "axios": "^0.21.4",


@@ 16,6 16,7 @@
    "jsonwebtoken": "^8.5.1",
    "morgan": "~1.9.1",
    "nodemailer": "^6.6.3",
    "pg": "^8.7.1",
    "pug": "^3.0.2",
    "uuid": "^8.3.2"
  }

M www/routes/deploy.js => www/routes/deploy.js +7 -7
@@ 8,18 8,18 @@ const requireAuth = require('../helpers/require-auth.js')
const router = express.Router()

router.get('/', requireAuth, function(req, res, next) {
    res.render('deploy', { userEmail: req.userEmail } )
    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
    const user = req.useremail

    if (!name || !repo || !user) {
	return res.status(400).render('error', {
	    message: '400 - Invalid Request',
	    userEmail: req.userEmail,
	    userEmail: req.useremail,
	    error: {
		status: 'The server doesn\'t know what to do.',
		stack: req.url,


@@ 63,11 63,11 @@ router.get('/status/:jobID', requireAuth, async function(req, res, next) {
	    if (code === 0) {
		return res.status(200).render('status-success', {
		    status: status,
		    userEmail: req.userEmail
		    userEmail: req.useremail
		})
	    } else {
		return res.status(500).render('status-fail', {
		    userEmail: req.userEmail,
		    userEmail: req.useremail,
		    status: status
		})
	    }


@@ 75,7 75,7 @@ router.get('/status/:jobID', requireAuth, async function(req, res, next) {

	return res.status(200).render('status', {
	    status: status,
	    userEmail: req.userEmail
	    userEmail: req.useremail
	})
    } catch (err) {
	return res.status(500).render('error', {


@@ 83,7 83,7 @@ router.get('/status/:jobID', requireAuth, async function(req, res, next) {
	    error: {
		status: 'An error occured getting the build status. Please send Zeke an email if this persists.',
		stack: err,
		userEmail: req.userEmail
		userEmail: req.useremail
	    }
	})
    }

M www/routes/index.js => www/routes/index.js +8 -15
@@ 21,6 21,7 @@ const jwt = require('jsonwebtoken')

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

const emailAllowlist = ['zekemedley@gmail.com', 'zekemedley@berkeley.edu']



@@ 36,18 37,9 @@ const transporter = nodemailer.createTransport({

const tokenMap = new KV()

router.get('/', requireAuth, function(req, res, next) {
    // TODO: this userWebpages field needs to be populated from the
    // database.
    res.render('index', { userEmail: req.userEmail, userWebpages: [{
	name: 'cat',
	clearnet: 'https://cat.negativefour.app',
	union: 'http://omvczo4whdvaikbb55rpn3ilnlalz3awtruzbsvy4fnwk3edgyx2zaqd.onion'
    }, {
	name: 'fish',
	clearnet: 'https://fish.negativefour.app',
	union: 'http://4ymobdh6bzbritm5oc6o7lgno6u6yit6kg4uc4jlhslficmejx6cw7qd.onion'
    }] });
router.get('/', requireAuth, async function(req, res, next) {
    const deployments = await db.getUserDeployments(req.useremail)
    res.render('index', { userEmail: req.username, userWebpages: deployments });
});

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


@@ 60,7 52,7 @@ router.get('/login', function(req, res, next) {
router.post('/login', function(req, res, next) {
    if (req.body.email) {
	res.render('email-submitted', { email: req.body.email })
	if (emailAllowlist.includes(req.body.email)) {
	if (db.emailRegistered(req.body.email)) {
	    const token = uuid.v4();
	    tokenMap.put(token, req.body.email, 60*5)
	    transporter.sendMail({


@@ 84,12 76,13 @@ router.post('/login', function(req, res, next) {
    }
})

router.get('/email-auth/:token', function(req, res, next) {
router.get('/email-auth/:token', async function(req, res, next) {
    const token = req.params.token
    const email = tokenMap.get(token)
    if (email) {
	const signedToken = jwt.sign({
	    email: email,
	    username: await db.getNameFromEmail(email),
	}, process.env.JWT_KEY)
	res.cookie('NEGATIVEFOUR-AUTH', signedToken)
	res.redirect('/')


@@ 109,7 102,7 @@ router.get('/logout', function(req, res, next) {
})

router.get('/deploy', requireAuth, function(req, res, next) {
    res.render('deploy', { userEmail: req.userEmail } )
    res.render('deploy', { userEmail: req.useremail } )
})

module.exports = router;

M www/views/index.pug => www/views/index.pug +1 -1
@@ 12,5 12,5 @@ block content
        h3= page.name
        a(href=`${page.clearnet}` target='_blank')
          img(src='/images/world-wide-web.svg')
        a(href=`${page.union}` target='_blank')
        a(href=`${page.darknet}` target='_blank')
          img(src='/images/tor.svg')