From 733fd82de5d0448649f769af5850ad6cc528f0d5 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Thu, 21 May 2020 09:13:11 -0700 Subject: [PATCH] Add a simple /stats endpoint to get a count of current games in progress This view is particularly spartan so far, (just two lines of next, not even HTML). Most of the work in this commit is actually setting up the authentication mechanism, since /stats is the first page we have that requires a user to be authenticated (and to also have the "admin" role). We have a nice-looking "/login" page with proper styling and clean messages for login failure. If an unauthenticated user goes to /stats they will be sent to /login?next=/stats and after successfully authenticating, will be sent back to /stats (this time getting the spartan view of the game statistics). There is another set of pages that is more minmal than we really want. This is all in the area of user that successfully authenticates but doesn't have the "admin" role. I'm ignoring all of these issues for now because I'm not going to actually configure any such users. But here are the issues: * If a user without the admin role hits /stats they they will get a correct 401 status, but a very spartan page (just the word "Unauthorized" as plain text). * In that case, if the user wants to logout there are no links provided to do that. * There _is_ a page at /logout which does do a correct logout, but again returns a very spartan, plain-text message that you are logged out. --- lmno.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- login.html | 42 ++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 login.html diff --git a/lmno.js b/lmno.js index 65c317d..abcdba2 100644 --- a/lmno.js +++ b/lmno.js @@ -2,6 +2,8 @@ const express = require("express"); const cors = require("cors"); const body_parser = require("body-parser"); const session = require("express-session"); +const bcrypt = require("bcrypt"); +const path = require("path"); try { var lmno_config = require("./lmno-config.json"); @@ -16,15 +18,22 @@ function config_usage() { Please create a file named lmno-config.json that looks as follows: { - "session_secret": ""; + "session_secret": "", + "users": { + "username": "", + "password_hash_bcrypt": "" + } } -Note: Don't use the exact text above, but instead replace the string -with what it describes: a long string of random characters.`); +Note: Of course, change all of to actual values desired. + +The "node lmno-passwd.js" command can help generate password hashes.`); } const app = express(); app.use(cors()); +app.use(body_parser.urlencoded({ extended: false })); +app.use(body_parser.json()); app.use(session({ secret: lmno_config.session_secret, resave: false, @@ -145,6 +154,73 @@ app.use('/empires/:game_id([a-zA-Z0-9]{4})', (request, response, next) => { next(); }); +function auth_admin(request, response, next) { + /* If there is no user associated with this session, redirect to the login + * page (and set a "next" query parameter so we can come back here). + */ + if (! request.session.user) { + response.redirect(302, "/login?next=" + request.path); + return; + } + + /* If the user is logged in but not authorized to view the page then + * we return that error. */ + if (request.session.user.role !== "admin") { + response.status(401).send("Unauthorized"); + return; + } + next(); +} + +app.get('/logout', (request, response) => { + request.session.user = undefined; + + response.send("You are now logged out."); +}); + +app.get('/login', (request, response) => { + if (request.session.user) { + response.send("Welcome, " + request.session.user + "."); + return; + } + + response.sendFile(path.join(__dirname, './login.html')); +}); + +app.post('/login', async (request, response) => { + const username = request.body.username; + const password = request.body.password; + const user = lmno_config.users[username]; + if (! user) { + response.sendStatus(404); + return; + } + const match = await bcrypt.compare(password, user.password_hash_bcrypt); + if (! match) { + response.sendStatus(404); + return; + } + request.session.user = { username: user.username, role: user.role }; + response.sendStatus(200); + return; +}); + +/* A stats page (only available to admin users) */ +app.get('/stats/', auth_admin, (request, response) => { + let active = 0; + let idle = 0; + + for (let id in lmno.ids) { + if (lmno.ids[id].game.clients.length) + active++; + else + idle++; + } + response.send(`Active games: ${active}.
+Idle games: ${idle}`); +}); + + /* Mount sub apps. only _after_ we have done all the middleware we need. */ app.use('/empires/[a-zA-Z0-9]{4}/', empires.app); diff --git a/login.html b/login.html new file mode 100644 index 0000000..f78e679 --- /dev/null +++ b/login.html @@ -0,0 +1,42 @@ + + + + + + + LMNO: Login + + + + + + + + +
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ + -- 2.43.0