diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..b735c1e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "env": { + "node": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2020 + }, + "rules": { + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "no-console": "off" + } +} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 5be1df1..0000000 --- a/.jshintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "node": true, - "browser": true, - "unused": true, - "multistr": true, - "globalstrict": true, - "predef": [ "angular", "$" ], - "esnext": true -} diff --git a/backend/cors.js b/backend/cors.js new file mode 100644 index 0000000..bae7620 --- /dev/null +++ b/backend/cors.js @@ -0,0 +1,56 @@ +/* jshint node:true */ + +'use strict'; + +const url = require('url'); + +/* + * CORS middleware + * + * options can contains a list of origins + */ +module.exports = function cors(options) { + options = options || { }; + const maxAge = options.maxAge || 60 * 60 * 25 * 5; // 5 days + const origins = options.origins || [ '*' ]; + const allowCredentials = options.allowCredentials || false; // cookies + + return function (req, res, next) { + let requestOrigin = req.headers.origin; + if (!requestOrigin) return next(); + + requestOrigin = url.parse(requestOrigin); + if (!requestOrigin.host) return res.status(405).send('CORS not allowed from this domain'); + + const hostname = requestOrigin.host.split(':')[0]; // remove any port + const originAllowed = origins.some(function (o) { return o === '*' || o === hostname; }); + if (!originAllowed) { + return res.status(405).send('CORS not allowed from this domain'); + } + + // respond back with req.headers.origin which might contain the scheme + res.header('Access-Control-Allow-Origin', req.headers.origin); + res.header('Access-Control-Allow-Credentials', allowCredentials); + + // handle preflighted requests + if (req.method === 'OPTIONS') { + if (req.headers['access-control-request-method']) { + res.header('Access-Control-Allow-Methods', 'GET, PUT, DELETE, POST, OPTIONS'); + } + + if (req.headers['access-control-request-headers']) { + res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']); + } + + res.header('Access-Control-Max-Age', maxAge); + + return res.status(200).send(); + } + + if (req.headers['access-control-request-headers']) { + res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']); + } + + next(); + }; +}; diff --git a/backend/database.js b/backend/database.js index d53f9f5..d0bc514 100644 --- a/backend/database.js +++ b/backend/database.js @@ -157,51 +157,36 @@ function projectsRemoveAll(userId, callback) { }); } -function usersList(callback) { - assert.strictEqual(typeof callback, 'function'); - - db.query('SELECT * FROM users', [], function (error, result) { - if (error) return callback(error); - callback(null, result); - }); +async function usersList() { + return await db.query('SELECT * FROM users', []); } -function usersAdd(user, callback) { +async function usersAdd(user) { assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof callback, 'function'); - db.query('INSERT INTO users SET ?', user, function (error) { - if (error) return callback(error); - callback(null, user); - }); + await db.query('INSERT INTO users SET ?', user); + + return user; } -function usersGet(userId, callback) { +async function usersGet(userId) { assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); - db.query('SELECT * FROM users WHERE id=?', [ userId ], function (error, result) { - if (error) return callback(error); - callback(null, result[0]); - }); + const result = await db.query('SELECT * FROM users WHERE id=?', [ userId ]); + if (!result.length) throw new Error('no such user'); + + return result[0]; } -function usersUpdate(userId, data, callback) { +async function usersUpdate(userId, data) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - db.query('UPDATE users SET email=?, githubToken=? WHERE id=?', [ data.email, data.githubToken, userId ], function (error, result) { - if (error) return callback(error); - callback(null); - }); + await db.query('UPDATE users SET email=?, githubToken=? WHERE id=?', [ data.email, data.githubToken, userId ]); } -function usersRemove(userId, callback) { +function usersRemove(userId) { assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - callback(); } function releasesList(projectId, callback) { diff --git a/backend/routes.js b/backend/routes.js index a5893a9..f7608ee 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -1,8 +1,6 @@ 'use strict'; var assert = require('assert'), - path = require('path'), - fs = require('fs'), database = require('./database.js'), github = require('./github.js'), tasks = require('./tasks.js'), @@ -45,10 +43,22 @@ function status(req, res, next) { next(new HttpSuccess(200, {})); } -function auth(req, res, next) { +async function auth(req, res, next) { if (!req.oidc.isAuthenticated()) return next(new HttpError(401, 'Unauthorized')); - req.user = req.oidc.user; + let user; + try { + user = await database.users.get(req.oidc.user.sub); + } catch (e) { + try { + user = await database.users.add({ id: req.oidc.user.sub, email: req.oidc.user.email, githubToken: '' }); + } catch (e) { + console.error('Failed to add user', req.user.oidc.user, e); + return next(new HttpError(500, 'internal error')); + } + } + + req.user = user; next(); } diff --git a/backend/server.js b/backend/server.js index dcff814..ec76fdb 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,9 +2,9 @@ var assert = require('assert'), connectTimeout = require('connect-timeout'), + cors = require('./cors.js'), lastMile = require('connect-lastmile'), oidc = require('express-openid-connect'), - HttpSuccess = lastMile.HttpSuccess, path = require('path'), fs = require('fs'), routes = require('./routes.js'), @@ -37,6 +37,9 @@ function start(port, callback) { app.set('trust proxy', 1); + // currently for local development. vite runs on http://localhost:5173 + app.use(cors({ origins: [ '*' ], allowCredentials: true })); + app.use(connectTimeout(10000, { respond: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -60,9 +63,12 @@ function start(port, callback) { })); } else { // mock oidc + let loginSession = false; + app.use((req, res, next) => { res.oidc = { login(options) { + loginSession = true; res.redirect(options.authorizationParams.redirect_uri); } }; @@ -78,15 +84,20 @@ function start(port, callback) { email_verified: true }, isAuthenticated() { - return true; + return loginSession; } - } + }; next(); }); app.use('/api/v1/callback', (req, res) => { - res.redirect(req.query.returnTo || '/'); + res.redirect(`http://localhost:${process.env.VITE_DEV_PORT || process.env.PORT}/`); + }); + + app.use('/api/v1/logout', (req, res) => { + loginSession = false; + res.status(200).send({}); }); } app.use(router); diff --git a/develop.sh b/develop.sh index 6d15cb9..191c122 100755 --- a/develop.sh +++ b/develop.sh @@ -47,15 +47,13 @@ cat < ./database.json } EOF -echo "=> Build and watch frontend in the background" -npm run watch & - echo "=> Run database migrations" ./node_modules/.bin/db-migrate up -echo "============================" -echo " Open http://localhost:3000 " -echo "============================" +echo "========================================================" +echo "If running the vite dev server as below in a second terminal on the side for live-reload, set VITE_DEV_PORT to the port vite runs on." +echo "VITE_API_ORIGIN=http://localhost:3000 npm run dev" +echo "========================================================" echo "=> Start releasebell" ./index.js \ No newline at end of file diff --git a/frontend/App.vue b/frontend/App.vue index 1d20fda..66dc87a 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -3,38 +3,203 @@

Release Bell

-
+ + + + + +