diff --git a/config.json.example b/config.json.example index 6a581f92..e61c523b 100644 --- a/config.json.example +++ b/config.json.example @@ -8,6 +8,7 @@ "myAddress": "localhost", "httpPort": 80, "httpsPort": 443, + "administratorNames": [], "autoCreateAccount": true, "skipStoryModeChoice": true, "skipTutorial": true, diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 6f81fbaa..51d3815f 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -6,7 +6,7 @@ import buildConfig from "@/static/data/buildConfig.json"; import { toLoginRequest } from "@/src/helpers/loginHelpers"; import { Account } from "@/src/models/loginModel"; -import { createAccount, isCorrectPassword } from "@/src/services/loginService"; +import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { IDatabaseAccountJson, ILoginResponse } from "@/src/types/loginTypes"; import { DTLS, groups, HUB, platformCDNs } from "@/static/fixed_responses/login_static"; import { logger } from "@/src/utils/logger"; @@ -26,10 +26,19 @@ export const loginController: RequestHandler = async (request, response) => { if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { try { + const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); + let name = nameFromEmail; + if (await isNameTaken(name)) { + let suffix = 0; + do { + ++suffix; + name = nameFromEmail + suffix; + } while (await isNameTaken(name)); + } const newAccount = await createAccount({ email: loginRequest.email, password: loginRequest.password, - DisplayName: loginRequest.email.substring(0, loginRequest.email.indexOf("@")), + DisplayName: name, CountryCode: loginRequest.lang.toUpperCase(), ClientType: loginRequest.ClientType, CrossPlatformAllowed: true, diff --git a/src/controllers/custom/createAccountController.ts b/src/controllers/custom/createAccountController.ts index 723b31c9..bac12508 100644 --- a/src/controllers/custom/createAccountController.ts +++ b/src/controllers/custom/createAccountController.ts @@ -1,14 +1,16 @@ import { toCreateAccount, toDatabaseAccount } from "@/src/helpers/customHelpers/customHelpers"; -import { createAccount } from "@/src/services/loginService"; +import { createAccount, isNameTaken } from "@/src/services/loginService"; import { RequestHandler } from "express"; const createAccountController: RequestHandler = async (req, res) => { const createAccountData = toCreateAccount(req.body); - const databaseAccount = toDatabaseAccount(createAccountData); - - const account = await createAccount(databaseAccount); - - res.json(account); + if (await isNameTaken(createAccountData.DisplayName)) { + res.status(409).json("Name already in use"); + } else { + const databaseAccount = toDatabaseAccount(createAccountData); + const account = await createAccount(databaseAccount); + res.json(account); + } }; export { createAccountController }; diff --git a/src/controllers/custom/getConfigDataController.ts b/src/controllers/custom/getConfigDataController.ts index 9a8684ca..12208527 100644 --- a/src/controllers/custom/getConfigDataController.ts +++ b/src/controllers/custom/getConfigDataController.ts @@ -1,8 +1,14 @@ import { RequestHandler } from "express"; import { config } from "@/src/services/configService"; +import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; -const getConfigDataController: RequestHandler = (_req, res) => { - res.json(config); +const getConfigDataController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + if (isAdministrator(account)) { + res.json(config); + } else { + res.status(401).end(); + } }; export { getConfigDataController }; diff --git a/src/controllers/custom/renameAccountController.ts b/src/controllers/custom/renameAccountController.ts index 1631cc02..c5b733e8 100644 --- a/src/controllers/custom/renameAccountController.ts +++ b/src/controllers/custom/renameAccountController.ts @@ -1,12 +1,16 @@ import { RequestHandler } from "express"; -import { getAccountForRequest } from "@/src/services/loginService"; +import { getAccountForRequest, isNameTaken } from "@/src/services/loginService"; export const renameAccountController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); if (typeof req.query.newname == "string") { - account.DisplayName = req.query.newname; - await account.save(); - res.end(); + if (await isNameTaken(req.query.newname)) { + res.status(409).json("Name already in use"); + } else { + account.DisplayName = req.query.newname; + await account.save(); + res.end(); + } } else { res.status(400).end(); } diff --git a/src/controllers/custom/updateConfigDataController.ts b/src/controllers/custom/updateConfigDataController.ts index 3fdcc8e8..961cccb1 100644 --- a/src/controllers/custom/updateConfigDataController.ts +++ b/src/controllers/custom/updateConfigDataController.ts @@ -1,9 +1,15 @@ import { RequestHandler } from "express"; import { updateConfig } from "@/src/services/configService"; +import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; const updateConfigDataController: RequestHandler = async (req, res) => { - await updateConfig(String(req.body)); - res.end(); + const account = await getAccountForRequest(req); + if (isAdministrator(account)) { + await updateConfig(String(req.body)); + res.end(); + } else { + res.status(401).end(); + } }; export { updateConfigDataController }; diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 9b76a310..258497ad 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -24,7 +24,7 @@ const databaseAccountSchema = new Schema( { email: { type: String, required: true, unique: true }, password: { type: String, required: true }, - DisplayName: { type: String, required: true }, + DisplayName: { type: String, required: true, unique: true }, CountryCode: { type: String, required: true }, ClientType: { type: String }, CrossPlatformAllowed: { type: Boolean, required: true }, diff --git a/src/services/configService.ts b/src/services/configService.ts index a0aceb73..ecc6d359 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -14,6 +14,13 @@ fs.watchFile(configPath, () => { amnesia = false; } else { logger.info("Detected a change to config.json, reloading its contents."); + + // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory. + for (const key of Object.keys(config)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (config as any)[key] = undefined; + } + Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8"))); } }); @@ -25,6 +32,7 @@ interface IConfig { httpPort?: number; httpsPort?: number; myIrcAddresses?: string[]; + administratorNames?: string[]; autoCreateAccount?: boolean; skipStoryModeChoice?: boolean; skipTutorial?: boolean; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index c251409d..80f0b5b6 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -2,16 +2,21 @@ import { Account } from "@/src/models/loginModel"; import { createInventory } from "@/src/services/inventoryService"; import { IDatabaseAccount, IDatabaseAccountJson } from "@/src/types/loginTypes"; import { createShip } from "./shipService"; -import { Types } from "mongoose"; +import { Document, Types } from "mongoose"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { PersonalRooms } from "@/src/models/personalRoomsModel"; import new_personal_rooms from "@/static/fixed_responses/personalRooms.json"; import { Request } from "express"; +import { config } from "@/src/services/configService"; export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => { return requestPassword === databasePassword; }; +export const isNameTaken = async (name: string): Promise => { + return !!(await Account.findOne({ DisplayName: name })); +}; + export const createAccount = async (accountData: IDatabaseAccount): Promise => { const account = new Account(accountData); try { @@ -44,20 +49,21 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ await personalRooms.save(); }; -export const getAccountForRequest = async (req: Request) => { +// eslint-disable-next-line @typescript-eslint/ban-types +type TAccountDocument = Document & + IDatabaseAccountJson & { _id: Types.ObjectId; __v: number }; + +export const getAccountForRequest = async (req: Request): Promise => { if (!req.query.accountId) { throw new Error("Request is missing accountId parameter"); } if (!req.query.nonce || parseInt(req.query.nonce as string) === 0) { throw new Error("Request is missing nonce parameter"); } - const account = await Account.findOne( - { - _id: req.query.accountId, - Nonce: req.query.nonce - }, - "_id" - ); + const account = await Account.findOne({ + _id: req.query.accountId, + Nonce: req.query.nonce + }); if (!account) { throw new Error("Invalid accountId-nonce pair"); } @@ -67,3 +73,7 @@ export const getAccountForRequest = async (req: Request) => { export const getAccountIdForRequest = async (req: Request): Promise => { return (await getAccountForRequest(req))._id.toString(); }; + +export const isAdministrator = (account: TAccountDocument): boolean => { + return !!config.administratorNames?.find(x => x == account.DisplayName); +}; diff --git a/static/webui/index.html b/static/webui/index.html index 2ab99f23..416d1978 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -198,75 +198,80 @@
Server
-
-
- - +
+
+

You must be an administrator to use this feature. To become an administrator, add "" to administratorNames in the config.json.

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/static/webui/script.js b/static/webui/script.js index 1be39ceb..56736c38 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -792,7 +792,7 @@ const uiConfigs = [ ]; function doChangeSettings() { - fetch("/custom/config") + fetch("/custom/config?" + window.authz) .then(response => response.json()) .then(json => { for (const i of uiConfigs) { @@ -810,7 +810,7 @@ function doChangeSettings() { } } $.post({ - url: "/custom/config", + url: "/custom/config?" + window.authz, contentType: "text/plain", data: JSON.stringify(json, null, 2) }); @@ -820,23 +820,34 @@ function doChangeSettings() { // Cheats route single.getRoute("/webui/cheats").on("beforeload", function () { - fetch("/custom/config") - .then(response => response.json()) - .then(json => - Object.entries(json).forEach(entry => { - const [key, value] = entry; - var x = document.getElementById(`${key}`); - if (x != null) { - if (x.type == "checkbox") { - if (value === true) { - x.setAttribute("checked", "checked"); - } - } else if (x.type == "number") { - x.setAttribute("value", `${value}`); - } + let interval; + interval = setInterval(() => { + if (window.authz) { + clearInterval(interval); + fetch("/custom/config?" + window.authz).then(res => { + if (res.status == 200) { + $("#server-settings").removeClass("d-none"); + res.json().then(json => + Object.entries(json).forEach(entry => { + const [key, value] = entry; + var x = document.getElementById(`${key}`); + if (x != null) { + if (x.type == "checkbox") { + if (value === true) { + x.setAttribute("checked", "checked"); + } + } else if (x.type == "number") { + x.setAttribute("value", `${value}`); + } + } + }) + ); + } else { + $("#server-settings-no-perms").removeClass("d-none"); } - }) - ); + }); + } + }, 10); fetch("http://localhost:61558/ping", { mode: "no-cors" }) .then(() => {