Skip to content

Commit

Permalink
Merge branch 'svg-to-spritesheet' of https://github.com/Infinite-Ches…
Browse files Browse the repository at this point in the history
…s/infinitechess.org into svg-to-spritesheet
  • Loading branch information
Naviary2 committed Dec 12, 2024
2 parents a444315 + 875250e commit d2f5c96
Show file tree
Hide file tree
Showing 12 changed files with 416 additions and 13 deletions.
23 changes: 23 additions & 0 deletions src/client/css/admin.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
* {
background-color: rgb(20, 20, 20);
color: white;
}
body {
padding: 30px 40px;
}
textarea {
width: 100%;
height: 80vh;
}
.inputContainer {
display: flex;
flex-direction: row;
width: 100%;
}
input {
width: 400px;
}
button {
width: 150px;
margin-left: 4px;
}
2 changes: 2 additions & 0 deletions src/client/scripts/esm/util/jsutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ function copyPropertiesToObject(objSrc, objDest) {

/**
* O(1) method of checking if an object/dict is empty
* I think??? I may be wrong. I think before the first iteration of
* a for-in loop the program still has to calculate the keys...
* @param {Object} obj
* @returns {Boolean}
*/
Expand Down
31 changes: 31 additions & 0 deletions src/client/scripts/esm/views/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const commandInput = document.getElementById("commandInput")! as HTMLInputElement;
const commandHistory = document.getElementById("commandHistory")! as HTMLTextAreaElement;
const sendCommandButton = document.getElementById("sendButton")! as HTMLButtonElement;

async function sendCommand() {
const commandString: string = commandInput.value;
if (commandString.length === 0) return; // Don't send command if the input box is empty
commandInput.value = "";
const response = await fetch("command/" + commandString);
commandHistory.textContent += commandString + '\n' + await response.text() + "\n\n";
scrollToBottom(commandHistory);
}

function clickSubmitIfReturnPressed(event: any) {
// 13 is the key code for Enter key
if (event.keyCode === 13) sendCommandButton.click();
}

/**
* Automatically scrolls to the bottom of the container.
* @param container - The container to scroll.
*/
function scrollToBottom(container: HTMLElement) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth',
});
}

sendCommandButton.addEventListener("click", sendCommand);
commandInput.addEventListener('keyup', clickSubmitIfReturnPressed);
14 changes: 14 additions & 0 deletions src/client/views/admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<head>
<script defer src="scripts/esm/views/admin.js"></script>
<link rel="stylesheet" href="/css/admin.css" />
</head>
<body>
<textarea id="commandHistory" readonly ></textarea>
<div class="inputContainer">
<input id="commandInput" type="text" />
<button type="button" id="sendButton">
Send Command
</button>
</div>
</body>
269 changes: 269 additions & 0 deletions src/server/api/AdminPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@

/**
* This script handles all incoming commands send from the admin console page
* /admin
*/

import { manuallyVerifyUser } from "../controllers/verifyAccountController.js";
// @ts-ignore
import { getMemberDataByCriteria } from "../database/memberManager.js";
// @ts-ignore
import { logEvents } from "../middleware/logEvents.js";
// @ts-ignore
import { deleteAccount } from "../controllers/deleteAccountController.js";
// @ts-ignore
import { deleteAllSessionsOfUser } from "../controllers/authenticationTokens/sessionManager.js";
// @ts-ignore
import { areRolesHigherInPriority } from "../controllers/roles.js";

import type { CustomRequest } from "../../types.js";
import type { Response } from "express";



const validCommands = [
"ban",
"delete",
"username",
"logout",
"verify",
"post",
"invites",
"announce",
"userinfo",
"help"
];

function processCommand(req: CustomRequest, res: Response): void {
const command = req.params["command"]!;

const commandAndArgs = parseArgumentsFromCommand(command);

if (!req.memberInfo.signedIn) {
res.status(401).send("Cannot send commands while logged out.");
return;
}
if (!(req.memberInfo.roles?.includes("admin") ?? false)) {
res.status(403).send("Cannot send commands without the admin role");
return;
}
// TODO prevent affecting accounts with equal or higher roles
switch (commandAndArgs[0]) {
case "ban":
return;
case "delete":
deleteCommand(command, commandAndArgs, req, res);
return;
case "username":
usernameCommand(command, commandAndArgs, req, res);
return;
case "logout":
logoutUser(command, commandAndArgs, req, res);
return;
case "verify":
verify(command, commandAndArgs, req, res);
return;
case "post":
return;
case "invites":
return;
case "announce":
return;
case "userinfo":
getUserInfo(command, commandAndArgs, req, res);
return;
case "help":
helpCommand(commandAndArgs, res);
return;
default:
res.status(422).send("Unknown command.");
return;
}
}

function parseArgumentsFromCommand(command: string): string[] {
// Parse command
const commandAndArgs: string[] = [];
let inQuote: boolean = false;
let temp: string = "";
for (let i = 0; i < command.length; i++) {
if (command[i] === '"') {
if (i === 0 || command[i - 1] !== '\\') {
inQuote = !inQuote;
}
else {
temp += '"';
}
}
else if (command[i] === ' ' && !inQuote) {
commandAndArgs.push(temp);
temp = "";
}
else if (inQuote || (command[i] !== '"' && command[i] !== ' ')) {
temp += command[i];
}
}
commandAndArgs.push(temp);

return commandAndArgs;
}

function deleteCommand(command: string, commandAndArgs: string[], req: CustomRequest, res: Response) {
if (commandAndArgs.length < 3) {
res.status(422).send("Invalid number of arguments, expected 2, got " + (commandAndArgs.length - 1) + ".");
return;
}
// Valid Syntax
logCommand(command, req);
const reason = commandAndArgs[2];
const usernameArgument = commandAndArgs[1];
const { user_id, username, roles } = getMemberDataByCriteria(["user_id","username","roles"], "username", usernameArgument, { skipErrorLogging: true });
if (user_id === undefined) return sendAndLogResponse(res, 404, "User " + usernameArgument + " does not exist.");
// They were found...
const adminsRoles = req.memberInfo.signedIn ? req.memberInfo.roles : null;
const rolesOfAffectedUser = JSON.parse(roles);
// Don't delete them if they are equal or higher than your status
if (!areRolesHigherInPriority(adminsRoles, rolesOfAffectedUser)) return sendAndLogResponse(res, 403, "Forbidden to delete " + username + ".");
if (!deleteAccount(user_id, reason)) return sendAndLogResponse(res, 500, "Failed to delete " + username + ".");
sendAndLogResponse(res, 200, "Successfully deleted user " + username + ".");
}

function usernameCommand(command: string, commandAndArgs: string[], req: CustomRequest, res: Response) {
if (commandAndArgs[1] === "get") {
if (commandAndArgs.length < 3) {
res.status(422).send("Invalid number of arguments, expected 2, got " + (commandAndArgs.length - 1) + ".");
return;
}
const parsedId = Number.parseInt(commandAndArgs[2]!);
if (Number.isNaN(parsedId)) {
res.status(422).send("User id must be an integer.");
return;
}
// Valid Syntax
logCommand(command, req);
const { username } = getMemberDataByCriteria(["username"], "user_id", parsedId, { skipErrorLogging: true });
if (username === undefined) sendAndLogResponse(res, 404, "User with id " + parsedId + " does not exist.");
else sendAndLogResponse(res, 200, username);
}
else if (commandAndArgs[1] === "set") {
if (commandAndArgs.length < 4) {
res.status(422).send("Invalid number of arguments, expected 3, got " + (commandAndArgs.length - 1) + ".");
return;
}
// TODO add username changing logic
res.status(503).send("Changing usernames is not yet supported.");
}
else if (commandAndArgs[1] === undefined) {
res.status(422).send("Expected either get or set as a subcommand.");
}
else {
res.status(422).send("Invalid subcommand, expected either get or set, got " + commandAndArgs[1] + ".");
}
}

function logoutUser(command: string, commandAndArgs: string[], req: CustomRequest, res: Response) {
if (commandAndArgs.length < 2) {
res.status(422).send("Invalid number of arguments, expected 1, got " + (commandAndArgs.length - 1) + ".");
return;
}
// Valid Syntax
logCommand(command, req);
const usernameArgument = commandAndArgs[1];
const { user_id, username } = getMemberDataByCriteria(["user_id","username"], "username", usernameArgument, { skipErrorLogging: true });
if (user_id !== undefined) {
deleteAllSessionsOfUser(user_id);
sendAndLogResponse(res, 200, "User " + username + " successfully logged out."); // Use their case-sensitive username
}
else {
sendAndLogResponse(res, 404, "User " + usernameArgument + " does not exist.");
}
}

function verify(command: string, commandAndArgs: string[], req: CustomRequest, res: Response) {
if (commandAndArgs.length < 2) {
res.status(422).send("Invalid number of arguments, expected 1, got " + (commandAndArgs.length - 1) + ".");
return;
}
// Valid Syntax
logCommand(command, req);
const usernameArgument = commandAndArgs[1];
// This method works without us having to confirm they exist first
const result = manuallyVerifyUser(usernameArgument!); // { success, username, reason }
if (result.success) sendAndLogResponse(res, 200, "User " + result.username + " has been verified!");
else sendAndLogResponse(res, 500, result.reason); // Failure message
}

function getUserInfo(command: string, commandAndArgs: string[], req: CustomRequest, res: Response) {
if (commandAndArgs.length < 2) {
res.status(422).send("Invalid number of arguments, expected 1, got " + (commandAndArgs.length - 1) + ".");
return;
}
// Valid Syntax
logCommand(command, req);
const username = commandAndArgs[1];
const memberData = getMemberDataByCriteria(["user_id", "username", "roles", "joined", "last_seen", "preferences", "verification", "username_history"], "username", username, { skipErrorLogging: true });
if (Object.keys(memberData).length === 0) { // Empty (member not found)
sendAndLogResponse(res, 404, "User " + username + " does not exist.");
}
else {
sendAndLogResponse(res, 200, JSON.stringify(memberData));
}
}

function helpCommand(commandAndArgs: string[], res: Response) {
if (commandAndArgs.length === 1) {
res.status(200).send("Commands: " + validCommands.join(", ") + "\nUse help <command> to get more information about a command.");
return;
}
switch (commandAndArgs[1]) {
case "ban":
res.status(200).send("Syntax: ban <username> [days]\nBans a user for a duration or permanently.");
return;
case "unban":
res.status(200).send("Syntax: unban <email>\nUnbans the given email.");
return;
case "delete":
res.status(200).send("Syntax: delete <username> [reason]\nDeletes the given user's account for an optional reason.");
return;
case "username":
res.status(200).send("Syntax: username get <userid>\n username set <userid> <newUsername>\nGets or sets the username of the account with the given userid");
return;
case "logout":
res.status(200).send("Syntax: logout <username>\nLogs out all sessions of the account with the given username.");
return;
case "verify":
res.status(200).send("Syntax: verify <username>\nVerifies the account of the given username.");
return;
case "post":
return;
case "invites":
return;
case "announce":
return;
case "userinfo":
res.status(200).send("Syntax: userinfo <username>\nPrints info about a user.");
return;
case "help":
res.status(200).send("Syntax: help [command]\nPrints the list of commands or information about a command.");
return;
default:
res.status(422).send("Unknown command.");
return;
}
}

function logCommand(command: string, req: CustomRequest) {
if (req.memberInfo.signedIn) {
logEvents(`Command executed by admin "${req.memberInfo.username}" of id "${req.memberInfo.user_id}": ` + command, "adminCommands.txt", { print: true });
} else throw new Error('Admin SHOULD have been logged in by this point. DANGEROUS');
}

function sendAndLogResponse(res: Response, code: number, message: any) {
res.status(code).send(message);
// Also log the sent response
logEvents("Result: " + message + "\n", "adminCommands.txt", { print: true });
}

export {
processCommand
};
1 change: 1 addition & 0 deletions src/server/config/setupDev.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ async function createDevelopmentAccounts() {
if (!doesMemberOfUsernameExist("owner")) {
const user_id = await generateAccount({ username: "Owner", email: "email1", password: "1", autoVerify: true });
giveRole(user_id, "owner");
giveRole(user_id, "admin");
}
if (!doesMemberOfUsernameExist("patron")) {
const user_id = await generateAccount({ username: "Patron", email: "email2", password: "1", autoVerify: true });
Expand Down
Loading

0 comments on commit d2f5c96

Please sign in to comment.