From 1183f0f9b6c5d10a1f31a43d36f0de71fcf83748 Mon Sep 17 00:00:00 2001 From: Holly Date: Tue, 23 Apr 2024 00:58:07 +0100 Subject: [PATCH] start work on logged in pages + add auth --- server/config.example.json | 5 ++ server/index.ts | 117 +++++++++++++++++++++++++- server/interfaces/UsernameData.ts | 4 + server/objects/Config.ts | 7 ++ server/objects/SessionUser.ts | 9 ++ server/package-lock.json | 28 ++++++ server/package.json | 2 + server/repos/PartyRepo.ts | 4 +- server/repos/UserRepo.ts | 4 +- server/services/UserService.ts | 8 ++ server/templates/404.ejs | 4 +- server/templates/account/login.ejs | 17 ++-- server/templates/account/register.ejs | 27 ++++++ server/templates/base/footer.ejs | 18 ++++ server/templates/base/header.ejs | 22 ++++- server/templates/home.ejs | 36 +++++++- server/templates/index.ejs | 11 ++- 17 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 server/interfaces/UsernameData.ts create mode 100644 server/objects/SessionUser.ts create mode 100644 server/templates/account/register.ejs diff --git a/server/config.example.json b/server/config.example.json index 5161d38..811a55e 100644 --- a/server/config.example.json +++ b/server/config.example.json @@ -3,6 +3,11 @@ "http": 38194, "ws": 39195 }, + "session": { + "validity": 86400, + "length": 64, + "secret": "changeme" + }, "database": { "address": "localhost", "port": 3306, diff --git a/server/index.ts b/server/index.ts index e54d521..2bd6a4b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,8 @@ import { createReader, createWriter, Endian } from "bufferstuff"; import { WebSocketServer } from "ws"; import Fastify from "fastify"; +import FastifyFormBody from "@fastify/formbody"; +import FastifyCookie from "@fastify/cookie"; import FastifyView from "@fastify/view"; import EJS from "ejs"; import Config from "./objects/Config"; @@ -9,6 +11,11 @@ import RemoteUser from "./objects/RemoteUser"; import { MessageType } from "./enums/MessageType"; import Database from "./objects/Database"; import { Console } from "hsconsole"; +import UserService from "./services/UserService"; +import UsernameData from "./interfaces/UsernameData"; +import { randomBytes } from "crypto"; +import SessionUser from "./objects/SessionUser"; +import PasswordUtility from "./utilities/PasswordUtility"; Console.customHeader(`MultiProbe server started at ${new Date()}`); @@ -18,6 +25,17 @@ new Database(Config.database.address, Config.database.port, Config.database.user // Web stuff +const sessions = new FunkyArray(); +const sessionExpiryInterval = setInterval(() => { + const currentTime = Date.now(); + for (const key of sessions.keys) { + const session = sessions.get(key); + if (!session || (session && currentTime >= session.validityPeriod.getTime())) { + sessions.remove(key); + } + } +}, 3600000); + const fastify = Fastify({ logger: false }); @@ -26,9 +44,43 @@ fastify.register(FastifyView, { engine: { ejs: EJS } -}) +}); + +fastify.register(FastifyFormBody); + +fastify.register(FastifyCookie, { + secret: Config.session.secret, + parseOptions: { + path: "/", + secure: true + } +}); + +// Get Methods + +function validateSession(cookies:{ [cookieName: string]: string | undefined }) { + if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") { + const key = FastifyCookie.unsign(cookies["MP_SESSION"], Config.session.secret); + if (key.valid && sessions.has(key.value ?? "badkey")) { + return sessions.get(key.value ?? "badkey"); + } + } + + return undefined; +} fastify.get("/", async (req, res) => { + let session:SessionUser | undefined; + if (session = validateSession(req.cookies)) { + const user = await UserService.GetUser(session.userId); + //const groups = await UserService.GetUserParties(session.userId); + if (user) { + return res.view("templates/home.ejs", { user, parties: [] }); + } + + return res.view("templates/index.ejs", { }); + } + return res.view("templates/index.ejs", { }); }); @@ -45,7 +97,66 @@ fastify.get("/account/register", async (req, res) => { }); fastify.setNotFoundHandler(async (req, res) => { - return res.view("templates/404.ejs", { }); + return res.status(404).view("templates/404.ejs", { }); +}); + +// Post Methods + +fastify.post("/account/register", async (req, res) => { + const data = req.body as UsernameData; + if (typeof(data.username) !== "string" || typeof(data.password) !== "string" || data.username.length > 32 || data.password.length < 8) { + return res.view("templates/account/register.ejs", { }); + } + + const username = data.username.replaceAll("<", "<").replaceAll(">", ">"); + await UserService.CreateUser(0, username, data.password); + + const user = await UserService.GetUserByUsername(username); + if (!user) { + return res.view("templates/account/register.ejs", { }); + } + + const validPeriod = new Date(); + validPeriod.setTime(validPeriod.getTime() + Config.session.validity); + const key = randomBytes(Config.session.length).toString("hex"); + + sessions.set(key, new SessionUser(user.Id, validPeriod)); + + res.setCookie("MP_SESSION", key, { + path: "/", + signed: true + }); + + return res.redirect(302, "/"); +}); + +fastify.post("/account/login", async (req, res) => { + const data = req.body as UsernameData; + if (typeof(data.username) !== "string" || typeof(data.password) !== "string" || data.username.length > 32 || data.password.length < 8) { + return res.view("templates/account/login.ejs", { }); + } + + const user = await UserService.GetUserByUsername(data.username); + if (!user) { + return res.view("templates/account/login.ejs", { }); + } + + if (await PasswordUtility.ValidatePassword(user.PasswordHash, user.PasswordSalt, data.password)) { + const validPeriod = new Date(); + validPeriod.setTime(validPeriod.getTime() + Config.session.validity); + const key = randomBytes(Config.session.length).toString("hex"); + + sessions.set(key, new SessionUser(user.Id, validPeriod)); + + res.setCookie("MP_SESSION", key, { + path: "/", + signed: true + }); + + return res.redirect(302, "/"); + } + + return res.view("templates/account/login.ejs", { }); }); // Websocket stuff @@ -175,6 +286,8 @@ function shutdown() { Console.printInfo("Shutting down..."); websocketServer.close(async () => { await fastify.close(); + clearInterval(sessionExpiryInterval); + Console.cleanup(); console.log("Goodbye!"); }); diff --git a/server/interfaces/UsernameData.ts b/server/interfaces/UsernameData.ts new file mode 100644 index 0000000..2653431 --- /dev/null +++ b/server/interfaces/UsernameData.ts @@ -0,0 +1,4 @@ +export default interface UsernameData { + username?: string, + password?: string +} \ No newline at end of file diff --git a/server/objects/Config.ts b/server/objects/Config.ts index 9af9f8a..ebd5820 100644 --- a/server/objects/Config.ts +++ b/server/objects/Config.ts @@ -6,6 +6,7 @@ export default class Config { public constructor() { throw new Error("Static Class"); } public static ports:ConfigPorts = config.ports; + public static session:ConfigSession = config.session; public static database:ConfigDatabase = config.database; } @@ -14,6 +15,12 @@ interface ConfigPorts { ws: number } +interface ConfigSession { + validity: number, + length: number, + secret: string +} + interface ConfigDatabase { address: string, port: number, diff --git a/server/objects/SessionUser.ts b/server/objects/SessionUser.ts new file mode 100644 index 0000000..0de762f --- /dev/null +++ b/server/objects/SessionUser.ts @@ -0,0 +1,9 @@ +export default class SessionUser { + public readonly userId:number; + public readonly validityPeriod:Date; + + constructor(userId:number, validityPeriod:Date) { + this.userId = userId; + this.validityPeriod = validityPeriod; + } +} \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 44e836d..8f35920 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@fastify/cookie": "^9.3.1", + "@fastify/formbody": "^7.4.0", "@fastify/view": "^9.0.0", "bufferstuff": "^1.5.1", "ejs": "^3.1.10", @@ -52,6 +54,15 @@ "fast-uri": "^2.0.0" } }, + "node_modules/@fastify/cookie": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz", + "integrity": "sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==", + "dependencies": { + "cookie-signature": "^1.1.0", + "fastify-plugin": "^4.0.0" + } + }, "node_modules/@fastify/error": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", @@ -65,6 +76,15 @@ "fast-json-stringify": "^5.7.0" } }, + "node_modules/@fastify/formbody": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-7.4.0.tgz", + "integrity": "sha512-H3C6h1GN56/SMrZS8N2vCT2cZr7mIHzBHzOBa5OPpjfB/D6FzP9mMpE02ZzrFX0ANeh0BAJdoXKOF2e7IbV+Og==", + "dependencies": { + "fast-querystring": "^1.0.0", + "fastify-plugin": "^4.0.0" + } + }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -598,6 +618,14 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", + "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", diff --git a/server/package.json b/server/package.json index eb3b63e..c2e9747 100644 --- a/server/package.json +++ b/server/package.json @@ -15,6 +15,8 @@ "author": "tgpholly", "license": "MIT", "dependencies": { + "@fastify/cookie": "^9.3.1", + "@fastify/formbody": "^7.4.0", "@fastify/view": "^9.0.0", "bufferstuff": "^1.5.1", "ejs": "^3.1.10", diff --git a/server/repos/PartyRepo.ts b/server/repos/PartyRepo.ts index bf7a8a8..25f5f29 100644 --- a/server/repos/PartyRepo.ts +++ b/server/repos/PartyRepo.ts @@ -29,11 +29,11 @@ export default class PartyRepo { public static async insertUpdate(user:User) { if (user.Id === Number.MIN_VALUE) { await Database.Instance.query("INSERT Party (Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ - user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId, user.LastModifiedDatetime?.getTime(), user.DeletedByUserId, user.DeletedDatetime?.getTime(), Number(user.IsDeleted) + user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted) ]); } else { await Database.Instance.query(`UPDATE Party SET Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [ - user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId, user.LastModifiedDatetime?.getTime(), user.DeletedByUserId, user.DeletedDatetime?.getTime(), Number(user.IsDeleted), user.Id + user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id ]); } } diff --git a/server/repos/UserRepo.ts b/server/repos/UserRepo.ts index 04df783..b4d5ad5 100644 --- a/server/repos/UserRepo.ts +++ b/server/repos/UserRepo.ts @@ -28,11 +28,11 @@ export default class UserRepo { public static async insertUpdate(user:User) { if (user.Id === Number.MIN_VALUE) { await Database.Instance.query("INSERT User (Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ - user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId, user.LastModifiedDatetime?.getTime(), user.DeletedByUserId, user.DeletedDatetime?.getTime(), Number(user.IsDeleted) + user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted) ]); } else { await Database.Instance.query(`UPDATE User SET Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [ - user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId, user.LastModifiedDatetime?.getTime(), user.DeletedByUserId, user.DeletedDatetime?.getTime(), Number(user.IsDeleted), user.Id + user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id ]); } } diff --git a/server/services/UserService.ts b/server/services/UserService.ts index 8200491..e77833b 100644 --- a/server/services/UserService.ts +++ b/server/services/UserService.ts @@ -13,6 +13,14 @@ export default class UserService { } } + public static async GetUserByUsername(username:string) { + try { + return await UserRepo.selectByUsername(username); + } catch (e) { + Console.printError(`MultiProbe server service error:\n${e}`); + } + } + public static async GetParty(id:number) { try { return await PartyRepo.selectById(id); diff --git a/server/templates/404.ejs b/server/templates/404.ejs index 71c5ac2..8463cb0 100644 --- a/server/templates/404.ejs +++ b/server/templates/404.ejs @@ -1,3 +1,3 @@ -<%- include("base/header") -%> +<%- include("base/header", { title: "404"}) %>

404!

-<%- include("base/footer") -%> \ No newline at end of file +<%- include("base/footer") %> \ No newline at end of file diff --git a/server/templates/account/login.ejs b/server/templates/account/login.ejs index c0daa8e..053a78c 100644 --- a/server/templates/account/login.ejs +++ b/server/templates/account/login.ejs @@ -1,22 +1,27 @@ -<%- include("../base/header") -%> +<%- include("../base/header", { title: "Login" }) %>

Login

-
+
- +
- +
+ + +
+
-<%- include("../base/footer") -%> \ No newline at end of file +<%- include("../base/footer") %> \ No newline at end of file diff --git a/server/templates/account/register.ejs b/server/templates/account/register.ejs new file mode 100644 index 0000000..17198cb --- /dev/null +++ b/server/templates/account/register.ejs @@ -0,0 +1,27 @@ +<%- include("../base/header", { title: "Register" }) %> +

Register

+
+
+
+
+
+ + +
+
+ + +
+ + + +
+ +
+
+
+
+
+<%- include("../base/footer") %> \ No newline at end of file diff --git a/server/templates/base/footer.ejs b/server/templates/base/footer.ejs index fe3e814..b61939d 100644 --- a/server/templates/base/footer.ejs +++ b/server/templates/base/footer.ejs @@ -1,4 +1,22 @@ + + \ No newline at end of file diff --git a/server/templates/base/header.ejs b/server/templates/base/header.ejs index 25b1afe..b42b907 100644 --- a/server/templates/base/header.ejs +++ b/server/templates/base/header.ejs @@ -3,16 +3,19 @@ - Document + <%= title %> - MultiProbe + diff --git a/server/templates/home.ejs b/server/templates/home.ejs index be3e626..32c8adf 100644 --- a/server/templates/home.ejs +++ b/server/templates/home.ejs @@ -1,3 +1,33 @@ -<%- include("base/header") -%> -

Welcome back USER!

-<%- include("base/footer") -%> \ No newline at end of file +<%- include("base/header", { title: "Home", userId: user.Id }) %> +
+
+

Welcome back <%= user.Username %>!

+

What would you like to do?

+ +
+
+

Your Parties

+ <% if (parties.length > 0) { %> + + + + + + + +
Name
+ <% } else { %> + + <% } %> +
+
+<%- include("base/footer") %> \ No newline at end of file diff --git a/server/templates/index.ejs b/server/templates/index.ejs index 6294dd9..f1e3425 100644 --- a/server/templates/index.ejs +++ b/server/templates/index.ejs @@ -1,3 +1,8 @@ -<%- include("base/header") -%> -

Hello world!

-<%- include("base/footer") -%> \ No newline at end of file +<%- include("base/header", { title: "Home" }) %> +

MultiProbe

+

A way to explore Terminal 00 with friends.

+ +<%- include("base/footer") %> \ No newline at end of file