diff --git a/client/Terminal-00-Multiuser.user.js b/client/Terminal-00-Multiuser.user.js index 88ee9a3..018eec6 100644 --- a/client/Terminal-00-Multiuser.user.js +++ b/client/Terminal-00-Multiuser.user.js @@ -49,7 +49,8 @@ if (!window.TE_ACTIVE) { ClientJoined: 3, Clients: 4, ClientLeft: 5, - Ping: 6 + Ping: 6, + GroupData: 7 }; const styles = document.createElement("style"); @@ -211,12 +212,13 @@ if (!window.TE_ACTIVE) { const gotoButton = document.createElement("button"); gotoButton.innerText = "G"; + gotoButton.onclick = () => window.location.href = location; buttonBox.appendChild(gotoButton); groupUsers.appendChild(user); } - let clientWidth = document.body.getBoundingClientRect().width + let clientWidth = document.body.getBoundingClientRect().width; setInterval(() => { if (document.body.scrollHeight > window.innerHeight) { @@ -237,10 +239,6 @@ if (!window.TE_ACTIVE) { let oldMouseX = 0; let oldMouseY = 0; let lastSendTime = 0; - let username = ""; - if (localStorage["t00mp_username"]) { - username = localStorage["t00mp_username"]; - } class RemoteClient { constructor(name) { @@ -398,18 +396,23 @@ if (!window.TE_ACTIVE) { } animate(); - function doConnect() { + function doConnect(apiKey) { const Buffer = getBufferClass(); ws = new WebSocket(window.location.href.includes("//localhost:") ? "ws://localhost:38195" : "wss://ws.eusv.net/t00mp"); let keepAliveInterval; ws.onopen = () => { - selfCursor = new RemoteClient(username); + otherCursors.innerHTML = ""; + selfCursor = new RemoteClient(localStorage["t00mp_username"]); selfCursor.probeImage.style.visibility = "hidden"; selfCursor.element.style.visibility = localStorage["t00mp_cursorStyle"] ?? "hidden"; selfCursor.hasBeenMoved = true; 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(() => { ws.send(keepAlivePacket); }, 5000); @@ -473,22 +476,22 @@ if (!window.TE_ACTIVE) { } ws = undefined; ready = false; - setTimeout(doConnect, 5000); + setTimeout(() => doConnect(localStorage["mpapikey"]), 5000); } ws.onclose = onCloseAndError; ws.onerror = onCloseAndError; } - function createOnlineDialog() { + function createLoginDialog() { 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)"; 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); const title = document.createElement("h4"); - title.innerText = "T00 MultiUser"; + title.innerText = "MultiProbe"; dialog.appendChild(title); - const submitFunction = (event) => { + /*const submitFunction = (event) => { // Jank if (event && event.keyCode !== 13) { return; @@ -502,24 +505,32 @@ if (!window.TE_ACTIVE) { localStorage["t00mp_username"] = username.value; bg.remove(); window.location.href = window.location.href; - }; + };*/ + const loginForm = document.createElement("form"); + dialog.appendChild(loginForm); const username = document.createElement("input"); username.placeholder = "Enter Username"; username.maxLength = 32; username.style.width = "12rem"; - username.onkeypress = submitFunction; - if (localStorage["t00mp_username"]) { - username.value = localStorage["t00mp_username"]; - } - dialog.appendChild(username); + username.name = "username"; + //username.onkeypress = submitFunction; + loginForm.appendChild(username); + const password = document.createElement("input"); + 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"); buttons.style.marginTop = "1rem"; - dialog.appendChild(buttons); + loginForm.appendChild(buttons); const submitButton = document.createElement("button"); - submitButton.innerText = (!localStorage["t00mp_username"] || localStorage["t00mp_username"] === "") ? "Connect" : "Change Username"; - submitButton.onclick = () => submitFunction(null); + submitButton.innerText = "Connect"; + submitButton.type = "submit"; + //submitButton.onclick = () => submitFunction(null); buttons.appendChild(submitButton); - if (localStorage["t00mp_username"] !== "") { + /*if (localStorage["t00mp_username"] !== "") { const disconnectButton = document.createElement("button"); disconnectButton.innerText = "Disconnect"; disconnectButton.onclick = () => { @@ -528,22 +539,63 @@ if (!window.TE_ACTIVE) { window.location.href = window.location.href; } buttons.appendChild(disconnectButton); - } + }*/ const doNotButton = document.createElement("button"); doNotButton.innerText = "Close"; doNotButton.onclick = () => bg.remove(); doNotButton.style.marginLeft = "1rem"; 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); } 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.innerText = "MultiUser Menu"; - openMenuButton.onclick = () => createOnlineDialog(); + openMenuButton.innerText = "MultiProbe Menu"; + openMenuButton.onclick = () => createLoginDialog(); document.body.appendChild(openMenuButton); - if (username !== "") { - doConnect(); + if (localStorage["mpapikey"] && localStorage["mpapikey"] !== "") { + doConnect(localStorage["mpapikey"]); } + + /*if (username !== "") { + doConnect(); + }*/ })(); \ No newline at end of file diff --git a/server/enums/MessageType.ts b/server/enums/MessageType.ts index bfe83e9..a123e9c 100644 --- a/server/enums/MessageType.ts +++ b/server/enums/MessageType.ts @@ -5,5 +5,6 @@ export enum MessageType { ClientJoined, Clients, ClientLeft, - Ping + Ping, + GroupData } \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index f0a0a19..7090975 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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 const websocketServer = new WebSocketServer({ @@ -331,7 +351,12 @@ websocketServer.on("connection", (socket) => { if (user !== undefined) { return; } - const username = reader.readShortString(); + const apiKey = reader.readShortString(); + const dbUser = await UserService.GetUserByAPIKey(apiKey); + if (dbUser == null) { + return; + } + const rawURL = reader.readString(); let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", ""); if (page === "index") { @@ -349,8 +374,8 @@ websocketServer.on("connection", (socket) => { for (const otherUser of usersOnPage) { usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY); } - user = users.set(myUUID, new RemoteUser(socket, username, page, rawURL)); - sendToAllButSelf(user, createWriter(Endian.LE, 6 + username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(username).toBuffer()); + user = users.set(myUUID, new RemoteUser(socket, dbUser.Username, page, rawURL)); + sendToAllButSelf(user, createWriter(Endian.LE, 6 + dbUser.Username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(dbUser.Username).toBuffer()); user.send(usersToSend.toBuffer()); break; case MessageType.CursorPos: diff --git a/server/objects/User.ts b/server/objects/User.ts index c788d11..943d55a 100644 --- a/server/objects/User.ts +++ b/server/objects/User.ts @@ -3,6 +3,7 @@ export default class User { public Username:string; public PasswordSalt:string; public PasswordHash:string; + public APIKey:string; public CreatedByUserId:number; public CreatedDatetime:Date; public LastModifiedByUserId?:number; @@ -11,12 +12,13 @@ export default class User { public DeletedDatetime?:Date; 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) { - 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") { + 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(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.Username = username; this.PasswordHash = passwordHash; this.PasswordSalt = passwordSalt; + this.APIKey = apiKey; this.CreatedByUserId = createdByUserId; this.CreatedDatetime = createdDateTime; this.LastModifiedByUserId = lastModifiedByUserId; @@ -29,6 +31,7 @@ export default class User { this.Username = ""; this.PasswordHash = ""; this.PasswordSalt = ""; + this.APIKey = ""; this.CreatedByUserId = Number.MIN_VALUE; this.CreatedDatetime = new Date(0); this.IsDeleted = false; diff --git a/server/repos/UserRepo.ts b/server/repos/UserRepo.ts index b4d5ad5..4a9de68 100644 --- a/server/repos/UserRepo.ts +++ b/server/repos/UserRepo.ts @@ -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) { 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 ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted) + 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.APIKey, 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 ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.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.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.PasswordHash = dbUser.PasswordHash; user.PasswordSalt = dbUser.PasswordSalt; + user.APIKey = dbUser.APIKey; user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedDatetime = dbUser.CreatedDatetime; user.LastModifiedByUserId = dbUser.LastModifiedByUserId; diff --git a/server/services/UserService.ts b/server/services/UserService.ts index d9f6fbf..4d9769b 100644 --- a/server/services/UserService.ts +++ b/server/services/UserService.ts @@ -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) { try { return await PartyRepo.selectByUserId(userId);