diff --git a/CHANGELOG.md b/CHANGELOG.md index fae8712..307231e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + ### Added - CHANGELOG.md Changelog - content to the README.md - Chat to the bingo game (renderd with markdown-it) +- Postgres session storage +- sql-file directory `sql` + + +## Changed + +- changed export of `app.js` to the asynchronous init function that returns the app object +- `bin/www` now calls the init function of `app.js` + +### Removed + +- sqlite3 sesssion storage + +### Fixed + +- mobile layout diff --git a/app.js b/app.js index a4c080f..a6b7c73 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,8 @@ const createError = require('http-errors'), logger = require('morgan'), compileSass = require('express-compile-sass'), session = require('express-session'), - SQLiteStore = require('connect-sqlite3')(session), + pg = require('pg'), + pgSession = require('connect-pg-simple')(session), fsx = require('fs-extra'), yaml = require('js-yaml'), graphqlHTTP = require('express-graphql'), @@ -22,69 +23,86 @@ let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); if (fsx.existsSync('config.yaml')) Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); -let graphqlResolver = (request, response) => { - return { +async function init() { + // grapql default resolver + let graphqlResolver = (request, response) => { + return { time: Date.now(), bingo: bingoRouter.graphqlResolver(request, response) - } -}; -let app = express(); + } + }; + // database setup + let pgPool = new pg.Pool({ + host: settings.postgres.host, + port: settings.postgres.port, + user: settings.postgres.user, + password: settings.postgres.password, + database: settings.postgres.database + }); + await pgPool.query(fsx.readFileSync('./sql/createSessionTable.sql', 'utf-8')); -// view engine setup -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'pug'); -app.set('trust proxy', 1); + let app = express(); -app.use(logger('dev')); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(session({ - store: new SQLiteStore, - secret: settings.sessions.secret, - resave: false, - saveUninitialized: true, - cookie: { - expires: 10000000 - } -})); -app.use('/sass', compileSass({ - root: './public/stylesheets/sass', - sourceMap: true, - watchFiles: true, - logToConsole: true -})); -app.use(express.static(path.join(__dirname, 'public'))); + // view engine setup + app.set('views', path.join(__dirname, 'views')); + app.set('view engine', 'pug'); + app.set('trust proxy', 1); -app.use('/', indexRouter); -app.use('/users', usersRouter); -app.use(/\/riddle(\/.*)?/, riddleRouter); -app.use('/bingo', bingoRouter); -app.use('/graphql', graphqlHTTP((request, response) => { - return { - schema: buildSchema(importSchema('./graphql/schema.graphql')), - rootValue: graphqlResolver(request, response), - context: {session: request.session}, - graphiql: true - }; -})); + app.use(logger('dev')); + app.use(express.json()); + app.use(express.urlencoded({ extended: false })); + app.use(cookieParser()); + app.use(session({ + store: new pgSession({ + pool: pgPool, + tableName: 'user_sessions' + }), + secret: settings.sessions.secret, + resave: false, + saveUninitialized: true, + cookie: { + maxAge: 30 * 24 * 60 * 60 * 1000 // maxAge 30 days + } + })); + app.use('/sass', compileSass({ + root: './public/stylesheets/sass', + sourceMap: true, + watchFiles: true, + logToConsole: true + })); + app.use(express.static(path.join(__dirname, 'public'))); + + app.use('/', indexRouter); + app.use('/users', usersRouter); + app.use(/\/riddle(\/.*)?/, riddleRouter); + app.use('/bingo', bingoRouter); + app.use('/graphql', graphqlHTTP((request, response) => { + return { + schema: buildSchema(importSchema('./graphql/schema.graphql')), + rootValue: graphqlResolver(request, response), + context: {session: request.session}, + graphiql: true + }; + })); // catch 404 and forward to error handler -app.use(function(req, res, next) { - next(createError(404)); -}); + app.use(function(req, res, next) { + next(createError(404)); + }); // error handler -app.use(function(err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; - // render the error page - res.status(err.status || 500); - res.render('error'); -}); + // render the error page + res.status(err.status || 500); + res.render('error'); + }); + return app; +} -module.exports = app; +module.exports = init; //app.listen(settings.port); diff --git a/bin/www b/bin/www index 7f43cbf..9c787e9 100644 --- a/bin/www +++ b/bin/www @@ -4,7 +4,7 @@ * Module dependencies. */ -const app = require('../app'); +const appInit = require('../app'); const debug = require('debug')('whooshy:server'); const http = require('http'); const yaml = require('js-yaml'); @@ -26,25 +26,31 @@ try { */ let port = normalizePort(process.env.PORT || settings.port || '3000'); -app.set('port', port); -/** - * Create HTTP server. - */ +appInit().then((app) => { + app.set('port', port); -let server = http.createServer(app); + /** + * Create HTTP server. + */ -/** - * Listen on provided port, on all network interfaces. - */ + let server = http.createServer(app); -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); + /** + * Listen on provided port, on all network interfaces. + */ -/** - * Normalize a port into a number, string, or false. - */ + server.listen(port); + server.on('error', (error) => onError(error, server)); + server.on('listening', () => onListening(server)); + + /** + * Normalize a port into a number, string, or false. + */ +}).catch((err) => { + console.error(err.message); + console.error(err.stack); +}); function normalizePort(val) { let port = parseInt(val, 10); @@ -66,7 +72,7 @@ function normalizePort(val) { * Event listener for HTTP server "error" event. */ -function onError(error) { +function onError(error, server) { if (error.syscall !== 'listen') { throw error; } @@ -94,7 +100,7 @@ function onError(error) { * Event listener for HTTP server "listening" event. */ -function onListening() { +function onListening(server) { let addr = server.address(); let bind = typeof addr === 'string' ? 'pipe ' + addr diff --git a/default-config.yaml b/default-config.yaml index 9edb835..3155f66 100644 --- a/default-config.yaml +++ b/default-config.yaml @@ -2,4 +2,11 @@ sessions: secret: averysecuresessionsecret maxAge: 1000000 -port: 3000 \ No newline at end of file +port: 3000 + +postgres: + host: localhost + port: 5432 + user: whooshy + password: whooshypassword + database: whooshy diff --git a/package-lock.json b/package-lock.json index 2621680..ff946f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -381,6 +381,11 @@ } } }, + "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", @@ -572,6 +577,14 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "connect-pg-simple": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-5.0.0.tgz", + "integrity": "sha512-WZ7xkN+qe5bbDLgZ1L9GxnSbr155cJHmfNRzVR5hBvqio7Pg/vuH7Cf8lPUSFClQjtybYSejUqyO54sYt4cg+w==", + "requires": { + "pg": "^7.4.3" + } + }, "connect-sqlite3": { "version": "0.9.11", "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.11.tgz", @@ -2789,6 +2802,11 @@ "os-tmpdir": "^1.0.0" } }, + "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==" + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -2850,6 +2868,62 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "pg": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-7.11.0.tgz", + "integrity": "sha512-YO4V7vCmEMGoF390LJaFaohWNKaA2ayoQOEZmiHVcAUF+YsRThpf/TaKCgSvsSE7cDm37Q/Cy3Gz41xiX/XjTw==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "0.1.3", + "pg-pool": "^2.0.4", + "pg-types": "~2.0.0", + "pgpass": "1.x", + "semver": "4.3.2" + }, + "dependencies": { + "semver": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", + "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" + } + } + }, + "pg-connection-string": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", + "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" + }, + "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": "2.0.6", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-hod2zYQxM8Gt482q+qONGTYcg/qVcV32VHVPtktbBJs0us3Dj7xibISw0BAAXVMCzt8A/jhfJvpZaxUlqtqs0g==" + }, + "pg-types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.0.1.tgz", + "integrity": "sha512-b7y6QM1VF5nOeX9ukMQ0h8a9z89mojrBHXfJeSug4mhL0YpxNBm83ot2TROyoAmX/ZOX3UbwVO4EbH7i1ZZNiw==", + "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.2", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", + "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", + "requires": { + "split": "^1.0.0" + } + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -2873,6 +2947,29 @@ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "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.4", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.4.tgz", + "integrity": "sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA==" + }, + "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" + } + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -3528,6 +3625,14 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "requires": { + "through": "2" + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -3696,6 +3801,11 @@ "inherits": "2" } }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", diff --git a/package.json b/package.json index 176f489..02b64d5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "start": "node ./bin/www" }, "dependencies": { - "connect-sqlite3": "^0.9.11", + "connect-pg-simple": "^5.0.0", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", @@ -22,6 +22,7 @@ "markdown-it-emoji": "^1.4.0", "morgan": "~1.9.1", "node-sass": "^4.12.0", + "pg": "^7.11.0", "pug": "2.0.0-beta11" } } diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 59179f4..702a4d9 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -330,6 +330,17 @@ function submitOnEnter(event, func) { func(); } +/** + * Toggles the displayChat class on the content container to switch between chat-view and grid view + */ +function toggleChatView() { + let contentContainer = document.querySelector('#content-container'); + if (contentContainer.getAttribute('class') === 'displayChat') + contentContainer.setAttribute('class', ''); + else + contentContainer.setAttribute('class', 'displayChat') +} + window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { promiseRejectionEvent.promise.catch(err => console.log(err)); showError('Connection problems... Is the server down?'); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 07fb479..a8b1f55 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -21,12 +21,26 @@ textarea height: 80% #content-container grid-template-columns: 0 100% !important - grid-template-rows: 10% 80% 10% !important + grid-template-rows: 10% 40% 40% 10% !important #players-container, #chat-container display: none !important padding: 0 + .errorDiv + grid-column-start: 1 !important + grid-column-end: 4 !important + + #content-container.displayChat + grid-template-columns: 100% 0 !important + grid-template-rows: 0 25% 65% 10% !important + + #players-container, #chat-container + display: block !important + + #words-container + display: none !important + #username-form width: calc(100% - 2rem) !important left: 0 !important @@ -38,6 +52,23 @@ textarea width: calc(100% - 2rem) !important left: 0 !important + #button-container + grid-column-start: 2 !important + grid-column-end: 3 !important + + #chat-button-container + display: inline-block + grid-row-start: 4 + grid-row-end: 4 + grid-column-start: 1 + grid-column-end: 4 + overflow: hidden + margin: 0 0.5rem + + button + width: 100% + margin: 0.5rem 0 + @media(min-device-width: 641px) textarea height: 80% @@ -45,6 +76,8 @@ textarea #words-container width: 100% height: 100% + #chat-button-container + display: none .number-input width: 4rem @@ -179,7 +212,7 @@ textarea word-break: break-word #chat-content - height: calc(100% - 3.5rem) + height: calc(100% - 2.5rem) background-color: $primary overflow: auto @@ -199,7 +232,7 @@ textarea #chat-input width: 100% - margin: 1rem 0 0 0 + margin: 0 0 0 0 height: 2.5rem border-radius: 0 diff --git a/public/stylesheets/sass/mixins.sass b/public/stylesheets/sass/mixins.sass index af8b73b..0b60140 100644 --- a/public/stylesheets/sass/mixins.sass +++ b/public/stylesheets/sass/mixins.sass @@ -4,5 +4,4 @@ background: lighten($primary, 10%) color: $primarySurface border: 2px solid $primarySurface - border-radius: $borderRadius transition-duration: 0.2s diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 3b56a96..b86a1e9 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -12,11 +12,11 @@ @media (min-device-width: 641px) html - font-size: 4vw + font-size: 2.5vw @media (min-device-width: 961px) html - font-size: 3vw + font-size: 2.2vw @media (min-device-width: 1025px) html @@ -63,8 +63,10 @@ a ::-webkit-scrollbar-thumb background: darken($secondary, 5) - border-radius: 10px + transition-duration: 0.2s + +::-webkit-scrollbar-thumb:hover + background: $secondary ::-webkit-scrollbar-track background: lighten($primary, 5) - border-radius: 10px diff --git a/sql/createSessionTable.sql b/sql/createSessionTable.sql new file mode 100644 index 0000000..50b1234 --- /dev/null +++ b/sql/createSessionTable.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS "user_sessions" ( + "sid" varchar NOT NULL COLLATE "default", + "sess" json NOT NULL, + "expire" timestamp(6) NOT NULL, + PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE +) +WITH (OIDS=FALSE); diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index 1f9974a..213edc0 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -23,4 +23,6 @@ block content div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub=`${field.submitted}`) span= field.word div(id='button-container') - button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! + button(id='bingo-button', onclick='submitBingo()', class='hidden') Bingo! + div(id='chat-button-container') + button(id='chat-toggle-button', onclick='toggleChatView()') toggle Chat