]> git.cworth.org Git - empires-server/commitdiff
Add a simple /stats endpoint to get a count of current games in progress
authorCarl Worth <cworth@cworth.org>
Thu, 21 May 2020 16:13:11 +0000 (09:13 -0700)
committerCarl Worth <cworth@cworth.org>
Thu, 21 May 2020 16:13:11 +0000 (09:13 -0700)
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
login.html [new file with mode: 0644]

diff --git a/lmno.js b/lmno.js
index 65c317d05f37bb1b964781dd84b1b6d0f3b0f1ab..abcdba2ef220bd6aff8375bb3190d0cfbc87c727 100644 (file)
--- 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": "<this should be a long string of true-random characters>";
+  "session_secret": "<this should be a long string of true-random characters>",
+  "users": {
+    "username": "<username>",
+    "password_hash_bcrypt": "<password_hash_made_by_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 <these-parts> 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(`<html><body>Active games: ${active}.<br>
+Idle games: ${idle}</body></html>`);
+});
+
+
 /* 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 (file)
index 0000000..f78e679
--- /dev/null
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
+
+    <title>LMNO: Login</title>
+
+    <link rel="stylesheet" href="/reset.css" type="text/css" />
+    <link rel="stylesheet" href="/style.css" type="text/css" />
+  </head>
+  <body>
+
+    <script src="/lmno.js"></script>
+
+    <div id="page">
+
+      <div id="message-area">
+      </div>
+
+      <!-- The return false prevents the page from being reloaded -->
+      <form id="login-form" onsubmit="lmno_login(this); return false">
+        <div class="form-field large">
+          <label for="username">Username</label>
+          <input type="text" id="username" required>
+        </div>
+
+        <div class="form-field large">
+          <label for="Password">Password</label>
+          <input type="password" id="password" required>
+        </div>
+
+        <div class="form-field large">
+          <button type="submit">
+            Login
+          </button>
+        </div>
+      </form>
+
+    </div>
+  </body>
+</html>