rewrite to use account based auth

This commit is contained in:
Holly Stubbs 2024-04-25 02:37:37 +01:00
parent 0c27648d3b
commit 8507ca0840
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
6 changed files with 141 additions and 39 deletions

View file

@ -49,7 +49,8 @@ if (!window.TE_ACTIVE) {
ClientJoined: 3, ClientJoined: 3,
Clients: 4, Clients: 4,
ClientLeft: 5, ClientLeft: 5,
Ping: 6 Ping: 6,
GroupData: 7
}; };
const styles = document.createElement("style"); const styles = document.createElement("style");
@ -211,12 +212,13 @@ if (!window.TE_ACTIVE) {
const gotoButton = document.createElement("button"); const gotoButton = document.createElement("button");
gotoButton.innerText = "G"; gotoButton.innerText = "G";
gotoButton.onclick = () => window.location.href = location;
buttonBox.appendChild(gotoButton); buttonBox.appendChild(gotoButton);
groupUsers.appendChild(user); groupUsers.appendChild(user);
} }
let clientWidth = document.body.getBoundingClientRect().width let clientWidth = document.body.getBoundingClientRect().width;
setInterval(() => { setInterval(() => {
if (document.body.scrollHeight > window.innerHeight) { if (document.body.scrollHeight > window.innerHeight) {
@ -237,10 +239,6 @@ if (!window.TE_ACTIVE) {
let oldMouseX = 0; let oldMouseX = 0;
let oldMouseY = 0; let oldMouseY = 0;
let lastSendTime = 0; let lastSendTime = 0;
let username = "";
if (localStorage["t00mp_username"]) {
username = localStorage["t00mp_username"];
}
class RemoteClient { class RemoteClient {
constructor(name) { constructor(name) {
@ -398,18 +396,23 @@ if (!window.TE_ACTIVE) {
} }
animate(); animate();
function doConnect() { function doConnect(apiKey) {
const Buffer = getBufferClass(); const Buffer = getBufferClass();
ws = new WebSocket(window.location.href.includes("//localhost:") ? "ws://localhost:38195" : "wss://ws.eusv.net/t00mp"); ws = new WebSocket(window.location.href.includes("//localhost:") ? "ws://localhost:38195" : "wss://ws.eusv.net/t00mp");
let keepAliveInterval; let keepAliveInterval;
ws.onopen = () => { ws.onopen = () => {
selfCursor = new RemoteClient(username); otherCursors.innerHTML = "";
selfCursor = new RemoteClient(localStorage["t00mp_username"]);
selfCursor.probeImage.style.visibility = "hidden"; selfCursor.probeImage.style.visibility = "hidden";
selfCursor.element.style.visibility = localStorage["t00mp_cursorStyle"] ?? "hidden"; selfCursor.element.style.visibility = localStorage["t00mp_cursorStyle"] ?? "hidden";
selfCursor.hasBeenMoved = true; selfCursor.hasBeenMoved = true;
const currentPage = window.location.href.split("/").slice(3).join("/"); const currentPage = window.location.href.split("/").slice(3).join("/");
ws.send(createWriter(Endian.LE, 4 + username.length + currentPage.length).writeByte(MessageType.ClientDetails).writeShortString(username).writeString(currentPage).toBuffer()); ws.send(createWriter(Endian.LE, 4 + apiKey.length + currentPage.length)
.writeByte(MessageType.ClientDetails)
.writeShortString(apiKey)
.writeString(currentPage)
.toBuffer());
keepAliveInterval = setInterval(() => { keepAliveInterval = setInterval(() => {
ws.send(keepAlivePacket); ws.send(keepAlivePacket);
}, 5000); }, 5000);
@ -473,22 +476,22 @@ if (!window.TE_ACTIVE) {
} }
ws = undefined; ws = undefined;
ready = false; ready = false;
setTimeout(doConnect, 5000); setTimeout(() => doConnect(localStorage["mpapikey"]), 5000);
} }
ws.onclose = onCloseAndError; ws.onclose = onCloseAndError;
ws.onerror = onCloseAndError; ws.onerror = onCloseAndError;
} }
function createOnlineDialog() { function createLoginDialog() {
const bg = document.createElement("div"); const bg = document.createElement("div");
bg.style = "z-index:1000000000;position:fixed;top:0px;left:0px;width:100%;height:100%;background-color:rgba(0,0,0,0.5)"; bg.style = "z-index:1000000000;position:fixed;top:0px;left:0px;width:100%;height:100%;background-color:rgba(0,0,0,0.5)";
const dialog = document.createElement("div"); const dialog = document.createElement("div");
dialog.style = "position:absolute;top:50%;left:50%;width:15rem;height:9rem;background-color:#343434;transform:translate(-50%,-50%);text-align:center;color:white"; dialog.style = "position:absolute;top:50%;left:50%;width:15rem;background-color:#343434;padding-bottom:1rem;transform:translate(-50%,-50%);text-align:center;color:white";
bg.appendChild(dialog); bg.appendChild(dialog);
const title = document.createElement("h4"); const title = document.createElement("h4");
title.innerText = "T00 MultiUser"; title.innerText = "MultiProbe";
dialog.appendChild(title); dialog.appendChild(title);
const submitFunction = (event) => { /*const submitFunction = (event) => {
// Jank // Jank
if (event && event.keyCode !== 13) { if (event && event.keyCode !== 13) {
return; return;
@ -502,24 +505,32 @@ if (!window.TE_ACTIVE) {
localStorage["t00mp_username"] = username.value; localStorage["t00mp_username"] = username.value;
bg.remove(); bg.remove();
window.location.href = window.location.href; window.location.href = window.location.href;
}; };*/
const loginForm = document.createElement("form");
dialog.appendChild(loginForm);
const username = document.createElement("input"); const username = document.createElement("input");
username.placeholder = "Enter Username"; username.placeholder = "Enter Username";
username.maxLength = 32; username.maxLength = 32;
username.style.width = "12rem"; username.style.width = "12rem";
username.onkeypress = submitFunction; username.name = "username";
if (localStorage["t00mp_username"]) { //username.onkeypress = submitFunction;
username.value = localStorage["t00mp_username"]; loginForm.appendChild(username);
} const password = document.createElement("input");
dialog.appendChild(username); password.type = "password";
password.style.marginTop = ".5rem";
password.placeholder = "Enter Password";
password.style.width = "12rem";
password.name = "password";
loginForm.appendChild(password);
const buttons = document.createElement("div"); const buttons = document.createElement("div");
buttons.style.marginTop = "1rem"; buttons.style.marginTop = "1rem";
dialog.appendChild(buttons); loginForm.appendChild(buttons);
const submitButton = document.createElement("button"); const submitButton = document.createElement("button");
submitButton.innerText = (!localStorage["t00mp_username"] || localStorage["t00mp_username"] === "") ? "Connect" : "Change Username"; submitButton.innerText = "Connect";
submitButton.onclick = () => submitFunction(null); submitButton.type = "submit";
//submitButton.onclick = () => submitFunction(null);
buttons.appendChild(submitButton); buttons.appendChild(submitButton);
if (localStorage["t00mp_username"] !== "") { /*if (localStorage["t00mp_username"] !== "") {
const disconnectButton = document.createElement("button"); const disconnectButton = document.createElement("button");
disconnectButton.innerText = "Disconnect"; disconnectButton.innerText = "Disconnect";
disconnectButton.onclick = () => { disconnectButton.onclick = () => {
@ -528,22 +539,63 @@ if (!window.TE_ACTIVE) {
window.location.href = window.location.href; window.location.href = window.location.href;
} }
buttons.appendChild(disconnectButton); buttons.appendChild(disconnectButton);
} }*/
const doNotButton = document.createElement("button"); const doNotButton = document.createElement("button");
doNotButton.innerText = "Close"; doNotButton.innerText = "Close";
doNotButton.onclick = () => bg.remove(); doNotButton.onclick = () => bg.remove();
doNotButton.style.marginLeft = "1rem"; doNotButton.style.marginLeft = "1rem";
buttons.appendChild(doNotButton); buttons.appendChild(doNotButton);
loginForm.onsubmit = (e) => {
e.preventDefault();
username.disabled = true;
password.disabled = true;
submitButton.disabled = true;
doNotButton.disabled = true;
fetch(window.location.href.replace("127.0.0.1", "localhost").includes("//localhost:") ? "http://localhost:38194/api/login" : "https://multiprobe.eusv.net/api/login", {
method: "POST",
body: `username=${encodeURIComponent(username.value)}&password=${encodeURIComponent(password.value)}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}).then(res => {
if (res.status === 200) {
res.text().then(apiKey => {
localStorage["t00mp_username"] = username.value;
localStorage["mpapikey"] = apiKey;
doConnect(apiKey);
});
} else {
username.disabled = false;
password.disabled = false;
submitButton.disabled = false;
doNotButton.disabled = false;
}
}).catch(err => {
console.error(err);
username.disabled = false;
password.disabled = false;
submitButton.disabled = false;
doNotButton.disabled = false;
});
}
document.body.appendChild(bg); document.body.appendChild(bg);
} }
const openMenuButton = document.createElement("button"); const openMenuButton = document.createElement("button");
openMenuButton.style = "opacity:0.25;position:fixed;top:0px;right:0px;z-index:9999999;margin:4px;background-color:black;color:white;border:1px solid white"; openMenuButton.style = "opacity:0.25;position:fixed;top:0px;right:0px;z-index:9999999;margin:4px;background-color:black;color:white;border:1px solid white";
openMenuButton.innerText = "MultiUser Menu"; openMenuButton.innerText = "MultiProbe Menu";
openMenuButton.onclick = () => createOnlineDialog(); openMenuButton.onclick = () => createLoginDialog();
document.body.appendChild(openMenuButton); document.body.appendChild(openMenuButton);
if (username !== "") { if (localStorage["mpapikey"] && localStorage["mpapikey"] !== "") {
doConnect(); doConnect(localStorage["mpapikey"]);
} }
/*if (username !== "") {
doConnect();
}*/
})(); })();

View file

@ -5,5 +5,6 @@ export enum MessageType {
ClientJoined, ClientJoined,
Clients, Clients,
ClientLeft, ClientLeft,
Ping Ping,
GroupData
} }

View file

@ -271,6 +271,26 @@ fastify.post("/party/join", async (req, res) => {
} }
}); });
// API
fastify.post("/api/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.status(401).send("Username or Password incorrect");
}
const user = await UserService.GetUserByUsername(data.username);
if (!user) {
return res.status(401).send("Username or Password incorrect");
}
if (await PasswordUtility.ValidatePassword(user.PasswordHash, user.PasswordSalt, data.password)) {
return res.status(200).send(user.APIKey);
}
return res.status(401).send("Username or Password incorrect");
});
// Websocket stuff // Websocket stuff
const websocketServer = new WebSocketServer({ const websocketServer = new WebSocketServer({
@ -331,7 +351,12 @@ websocketServer.on("connection", (socket) => {
if (user !== undefined) { if (user !== undefined) {
return; return;
} }
const username = reader.readShortString(); const apiKey = reader.readShortString();
const dbUser = await UserService.GetUserByAPIKey(apiKey);
if (dbUser == null) {
return;
}
const rawURL = reader.readString(); const rawURL = reader.readString();
let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", ""); let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", "");
if (page === "index") { if (page === "index") {
@ -349,8 +374,8 @@ websocketServer.on("connection", (socket) => {
for (const otherUser of usersOnPage) { for (const otherUser of usersOnPage) {
usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY); usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY);
} }
user = users.set(myUUID, new RemoteUser(socket, username, page, rawURL)); user = users.set(myUUID, new RemoteUser(socket, dbUser.Username, page, rawURL));
sendToAllButSelf(user, createWriter(Endian.LE, 6 + username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(username).toBuffer()); sendToAllButSelf(user, createWriter(Endian.LE, 6 + dbUser.Username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(dbUser.Username).toBuffer());
user.send(usersToSend.toBuffer()); user.send(usersToSend.toBuffer());
break; break;
case MessageType.CursorPos: case MessageType.CursorPos:

View file

@ -3,6 +3,7 @@ export default class User {
public Username:string; public Username:string;
public PasswordSalt:string; public PasswordSalt:string;
public PasswordHash:string; public PasswordHash:string;
public APIKey:string;
public CreatedByUserId:number; public CreatedByUserId:number;
public CreatedDatetime:Date; public CreatedDatetime:Date;
public LastModifiedByUserId?:number; public LastModifiedByUserId?:number;
@ -11,12 +12,13 @@ export default class User {
public DeletedDatetime?:Date; public DeletedDatetime?:Date;
public IsDeleted:boolean; public IsDeleted:boolean;
public constructor(id?:number, username?:string, passwordSalt?:string, passwordHash?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) { public constructor(id?:number, username?:string, passwordSalt?:string, passwordHash?:string, apiKey?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) {
if (typeof(id) == "number" && typeof(username) == "string" && typeof(passwordHash) == "string" && typeof(passwordSalt) == "string" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") { if (typeof(id) == "number" && typeof(username) == "string" && typeof(passwordHash) == "string" && typeof(passwordSalt) == "string" && typeof(apiKey) == "string" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") {
this.Id = id; this.Id = id;
this.Username = username; this.Username = username;
this.PasswordHash = passwordHash; this.PasswordHash = passwordHash;
this.PasswordSalt = passwordSalt; this.PasswordSalt = passwordSalt;
this.APIKey = apiKey;
this.CreatedByUserId = createdByUserId; this.CreatedByUserId = createdByUserId;
this.CreatedDatetime = createdDateTime; this.CreatedDatetime = createdDateTime;
this.LastModifiedByUserId = lastModifiedByUserId; this.LastModifiedByUserId = lastModifiedByUserId;
@ -29,6 +31,7 @@ export default class User {
this.Username = ""; this.Username = "";
this.PasswordHash = ""; this.PasswordHash = "";
this.PasswordSalt = ""; this.PasswordSalt = "";
this.APIKey = "";
this.CreatedByUserId = Number.MIN_VALUE; this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0); this.CreatedDatetime = new Date(0);
this.IsDeleted = false; this.IsDeleted = false;

View file

@ -25,14 +25,25 @@ export default class UserRepo {
} }
} }
public static async selectByAPIKey(apiKey:string) {
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE APIKey = ? LIMIT 1", [apiKey]);
if (dbUser == null || dbUser.length === 0) {
return null;
} else {
const user = new User();
populateUserFromDB(user, dbUser[0]);
return user;
}
}
public static async insertUpdate(user:User) { public static async insertUpdate(user:User) {
if (user.Id === Number.MIN_VALUE) { if (user.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT User (Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ await Database.Instance.query("INSERT User (Username, PasswordHash, PasswordSalt, APIKey, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
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.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted)
]); ]);
} else { } else {
await Database.Instance.query(`UPDATE User SET Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [ await Database.Instance.query(`UPDATE User SET Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE 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 user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id
]); ]);
} }
} }
@ -43,6 +54,7 @@ function populateUserFromDB(user:User, dbUser:any) {
user.Username = dbUser.Username; user.Username = dbUser.Username;
user.PasswordHash = dbUser.PasswordHash; user.PasswordHash = dbUser.PasswordHash;
user.PasswordSalt = dbUser.PasswordSalt; user.PasswordSalt = dbUser.PasswordSalt;
user.APIKey = dbUser.APIKey;
user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = dbUser.CreatedDatetime; user.CreatedDatetime = dbUser.CreatedDatetime;
user.LastModifiedByUserId = dbUser.LastModifiedByUserId; user.LastModifiedByUserId = dbUser.LastModifiedByUserId;

View file

@ -26,6 +26,15 @@ export default class UserService {
} }
} }
public static async GetUserByAPIKey(apiKey:string) {
try {
return await UserRepo.selectByAPIKey(apiKey);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async GetUserParties(userId:number) { public static async GetUserParties(userId:number) {
try { try {
return await PartyRepo.selectByUserId(userId); return await PartyRepo.selectByUserId(userId);