XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFESpaceNinjaServer

feat: add administrators, require administrator perms to change server config in webui (#628)

103e9bc4
Sainan <sainan@calamity.inc>
提交于

代码差异

11 个文件 +174 -112
Modified config.json.example +1 -0
@@ -8,6 +8,7 @@
8 8 "myAddress": "localhost",
9 9 "httpPort": 80,
10 10 "httpsPort": 443,
11 "administratorNames": [],
11 12 "autoCreateAccount": true,
12 13 "skipStoryModeChoice": true,
13 14 "skipTutorial": true,
Modified src/controllers/api/loginController.ts +11 -2
@@ -6,7 +6,7 @@ import buildConfig from "@/static/data/buildConfig.json";
6 6
7 7 import { toLoginRequest } from "@/src/helpers/loginHelpers";
8 8 import { Account } from "@/src/models/loginModel";
9 import { createAccount, isCorrectPassword } from "@/src/services/loginService";
9 import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
10 10 import { IDatabaseAccountJson, ILoginResponse } from "@/src/types/loginTypes";
11 11 import { DTLS, groups, HUB, platformCDNs } from "@/static/fixed_responses/login_static";
12 12 import { logger } from "@/src/utils/logger";
@@ -26,10 +26,19 @@ export const loginController: RequestHandler = async (request, response) => {
26 26
27 27 if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
28 28 try {
29 const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
30 let name = nameFromEmail;
31 if (await isNameTaken(name)) {
32 let suffix = 0;
33 do {
34 ++suffix;
35 name = nameFromEmail + suffix;
36 } while (await isNameTaken(name));
37 }
29 38 const newAccount = await createAccount({
30 39 email: loginRequest.email,
31 40 password: loginRequest.password,
32 DisplayName: loginRequest.email.substring(0, loginRequest.email.indexOf("@")),
41 DisplayName: name,
33 42 CountryCode: loginRequest.lang.toUpperCase(),
34 43 ClientType: loginRequest.ClientType,
35 44 CrossPlatformAllowed: true,
Modified src/controllers/custom/createAccountController.ts +8 -6
@@ -1,14 +1,16 @@
1 1 import { toCreateAccount, toDatabaseAccount } from "@/src/helpers/customHelpers/customHelpers";
2 import { createAccount } from "@/src/services/loginService";
2 import { createAccount, isNameTaken } from "@/src/services/loginService";
3 3 import { RequestHandler } from "express";
4 4
5 5 const createAccountController: RequestHandler = async (req, res) => {
6 6 const createAccountData = toCreateAccount(req.body);
7 const databaseAccount = toDatabaseAccount(createAccountData);
8
9 const account = await createAccount(databaseAccount);
10
11 res.json(account);
7 if (await isNameTaken(createAccountData.DisplayName)) {
8 res.status(409).json("Name already in use");
9 } else {
10 const databaseAccount = toDatabaseAccount(createAccountData);
11 const account = await createAccount(databaseAccount);
12 res.json(account);
13 }
12 14 };
13 15
14 16 export { createAccountController };
Modified src/controllers/custom/getConfigDataController.ts +8 -2
@@ -1,8 +1,14 @@
1 1 import { RequestHandler } from "express";
2 2 import { config } from "@/src/services/configService";
3 import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
3 4
4 const getConfigDataController: RequestHandler = (_req, res) => {
5 res.json(config);
5 const getConfigDataController: RequestHandler = async (req, res) => {
6 const account = await getAccountForRequest(req);
7 if (isAdministrator(account)) {
8 res.json(config);
9 } else {
10 res.status(401).end();
11 }
6 12 };
7 13
8 14 export { getConfigDataController };
Modified src/controllers/custom/renameAccountController.ts +8 -4
@@ -1,12 +1,16 @@
1 1 import { RequestHandler } from "express";
2 import { getAccountForRequest } from "@/src/services/loginService";
2 import { getAccountForRequest, isNameTaken } from "@/src/services/loginService";
3 3
4 4 export const renameAccountController: RequestHandler = async (req, res) => {
5 5 const account = await getAccountForRequest(req);
6 6 if (typeof req.query.newname == "string") {
7 account.DisplayName = req.query.newname;
8 await account.save();
9 res.end();
7 if (await isNameTaken(req.query.newname)) {
8 res.status(409).json("Name already in use");
9 } else {
10 account.DisplayName = req.query.newname;
11 await account.save();
12 res.end();
13 }
10 14 } else {
11 15 res.status(400).end();
12 16 }
Modified src/controllers/custom/updateConfigDataController.ts +8 -2
@@ -1,9 +1,15 @@
1 1 import { RequestHandler } from "express";
2 2 import { updateConfig } from "@/src/services/configService";
3 import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
3 4
4 5 const updateConfigDataController: RequestHandler = async (req, res) => {
5 await updateConfig(String(req.body));
6 res.end();
6 const account = await getAccountForRequest(req);
7 if (isAdministrator(account)) {
8 await updateConfig(String(req.body));
9 res.end();
10 } else {
11 res.status(401).end();
12 }
7 13 };
8 14
9 15 export { updateConfigDataController };
Modified src/models/loginModel.ts +1 -1
@@ -24,7 +24,7 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
24 24 {
25 25 email: { type: String, required: true, unique: true },
26 26 password: { type: String, required: true },
27 DisplayName: { type: String, required: true },
27 DisplayName: { type: String, required: true, unique: true },
28 28 CountryCode: { type: String, required: true },
29 29 ClientType: { type: String },
30 30 CrossPlatformAllowed: { type: Boolean, required: true },
Modified src/services/configService.ts +8 -0
@@ -14,6 +14,13 @@ fs.watchFile(configPath, () => {
14 14 amnesia = false;
15 15 } else {
16 16 logger.info("Detected a change to config.json, reloading its contents.");
17
18 // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory.
19 for (const key of Object.keys(config)) {
20 // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
21 (config as any)[key] = undefined;
22 }
23
17 24 Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8")));
18 25 }
19 26 });
@@ -25,6 +32,7 @@ interface IConfig {
25 32 httpPort?: number;
26 33 httpsPort?: number;
27 34 myIrcAddresses?: string[];
35 administratorNames?: string[];
28 36 autoCreateAccount?: boolean;
29 37 skipStoryModeChoice?: boolean;
30 38 skipTutorial?: boolean;
Modified src/services/loginService.ts +19 -9
@@ -2,16 +2,21 @@ import { Account } from "@/src/models/loginModel";
2 2 import { createInventory } from "@/src/services/inventoryService";
3 3 import { IDatabaseAccount, IDatabaseAccountJson } from "@/src/types/loginTypes";
4 4 import { createShip } from "./shipService";
5 import { Types } from "mongoose";
5 import { Document, Types } from "mongoose";
6 6 import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
7 7 import { PersonalRooms } from "@/src/models/personalRoomsModel";
8 8 import new_personal_rooms from "@/static/fixed_responses/personalRooms.json";
9 9 import { Request } from "express";
10 import { config } from "@/src/services/configService";
10 11
11 12 export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => {
12 13 return requestPassword === databasePassword;
13 14 };
14 15
16 export const isNameTaken = async (name: string): Promise<boolean> => {
17 return !!(await Account.findOne({ DisplayName: name }));
18 };
19
15 20 export const createAccount = async (accountData: IDatabaseAccount): Promise<IDatabaseAccountJson> => {
16 21 const account = new Account(accountData);
17 22 try {
@@ -44,20 +49,21 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ
44 49 await personalRooms.save();
45 50 };
46 51
47 export const getAccountForRequest = async (req: Request) => {
52 // eslint-disable-next-line @typescript-eslint/ban-types
53 type TAccountDocument = Document<unknown, {}, IDatabaseAccountJson> &
54 IDatabaseAccountJson & { _id: Types.ObjectId; __v: number };
55
56 export const getAccountForRequest = async (req: Request): Promise<TAccountDocument> => {
48 57 if (!req.query.accountId) {
49 58 throw new Error("Request is missing accountId parameter");
50 59 }
51 60 if (!req.query.nonce || parseInt(req.query.nonce as string) === 0) {
52 61 throw new Error("Request is missing nonce parameter");
53 62 }
54 const account = await Account.findOne(
55 {
56 _id: req.query.accountId,
57 Nonce: req.query.nonce
58 },
59 "_id"
60 );
63 const account = await Account.findOne({
64 _id: req.query.accountId,
65 Nonce: req.query.nonce
66 });
61 67 if (!account) {
62 68 throw new Error("Invalid accountId-nonce pair");
63 69 }
@@ -67,3 +73,7 @@ export const getAccountForRequest = async (req: Request) => {
67 73 export const getAccountIdForRequest = async (req: Request): Promise<string> => {
68 74 return (await getAccountForRequest(req))._id.toString();
69 75 };
76
77 export const isAdministrator = (account: TAccountDocument): boolean => {
78 return !!config.administratorNames?.find(x => x == account.DisplayName);
79 };
Modified static/webui/index.html +73 -68
@@ -198,75 +198,80 @@
198 198 <div class="col-lg-4">
199 199 <div class="card mb-4">
200 200 <h5 class="card-header">Server</h5>
201 <form class="card-body" onsubmit="doChangeSettings();return false;">
202 <div class="form-check">
203 <input class="form-check-input" type="checkbox" id="skipStoryModeChoice" />
204 <label class="form-check-label" for="skipStoryModeChoice">Skip Story Mode Choice</label>
205 </div>
206 <div class="form-check">
207 <input class="form-check-input" type="checkbox" id="skipTutorial" />
208 <label class="form-check-label" for="skipTutorial">Skip Tutorial</label>
209 </div>
210 <div class="form-check">
211 <input class="form-check-input" type="checkbox" id="skipAllDialogue" />
212 <label class="form-check-label" for="skipAllDialogue">Skip All Dialogue</label>
213 </div>
214 <div class="form-check">
215 <input class="form-check-input" type="checkbox" id="unlockAllScans" />
216 <label class="form-check-label" for="unlockAllScans">Unlock All Scans</label>
217 </div>
218 <div class="form-check">
219 <input class="form-check-input" type="checkbox" id="unlockAllMissions" />
220 <label class="form-check-label" for="unlockAllMissions">Unlock All Missions</label>
221 </div>
222 <div class="form-check">
223 <input class="form-check-input" type="checkbox" id="unlockAllQuests" />
224 <label class="form-check-label" for="unlockAllQuests">Unlock All Quests</label>
225 </div>
226 <div class="form-check">
227 <input class="form-check-input" type="checkbox" id="completeAllQuests" />
228 <label class="form-check-label" for="completeAllQuests">Complete All Quests</label>
229 </div>
230 <div class="form-check">
231 <input class="form-check-input" type="checkbox" id="infiniteCredits" />
232 <label class="form-check-label" for="infiniteCredits">Infinite Credits</label>
233 </div>
234 <div class="form-check">
235 <input class="form-check-input" type="checkbox" id="infinitePlatinum" />
236 <label class="form-check-label" for="infinitePlatinum">Infinite Platinum</label>
237 </div>
238 <div class="form-check">
239 <input class="form-check-input" type="checkbox" id="unlockAllShipFeatures" />
240 <label class="form-check-label" for="unlockAllShipFeatures">Unlock All Ship Features</label>
241 </div>
242 <div class="form-check">
243 <input class="form-check-input" type="checkbox" id="unlockAllShipDecorations" />
244 <label class="form-check-label" for="unlockAllShipDecorations">Unlock All Ship Decorations</label>
245 </div>
246 <div class="form-check">
247 <input class="form-check-input" type="checkbox" id="unlockAllFlavourItems" />
248 <label class="form-check-label" for="unlockAllFlavourItems">
249 Unlock All <abbr title="Animation Sets, Glyphs, Plattes, etc.">Flavor Items</abbr>
250 </label>
251 </div>
252 <div class="form-check">
253 <input class="form-check-input" type="checkbox" id="unlockAllSkins" />
254 <label class="form-check-label" for="unlockAllSkins">Unlock All Skins</label>
255 </div>
256 <div class="form-check">
257 <input class="form-check-input" type="checkbox" id="universalPolarityEverywhere" />
258 <label class="form-check-label" for="universalPolarityEverywhere">
259 Universal Polarity Everywhere
260 </label>
261 </div>
262 <div class="form-group mt-2">
263 <label class="form-label" for="spoofMasteryRank">
264 Spoofed Mastery Rank (-1 to disable)
265 </label>
266 <input class="form-control" id="spoofMasteryRank" type="number" min="-1" />
201 <div class="card-body">
202 <div id="server-settings-no-perms" class="d-none">
203 <p>You must be an administrator to use this feature. To become an administrator, add <code>"<span class="displayname"></span>"</code> to <code>administratorNames</code> in the config.json.</p>
267 204 </div>
268 <button class="btn btn-primary mt-3" type="submit">Save Settings</button>
269 </form>
205 <form id="server-settings" class="d-none" onsubmit="doChangeSettings();return false;">
206 <div class="form-check">
207 <input class="form-check-input" type="checkbox" id="skipStoryModeChoice" />
208 <label class="form-check-label" for="skipStoryModeChoice">Skip Story Mode Choice</label>
209 </div>
210 <div class="form-check">
211 <input class="form-check-input" type="checkbox" id="skipTutorial" />
212 <label class="form-check-label" for="skipTutorial">Skip Tutorial</label>
213 </div>
214 <div class="form-check">
215 <input class="form-check-input" type="checkbox" id="skipAllDialogue" />
216 <label class="form-check-label" for="skipAllDialogue">Skip All Dialogue</label>
217 </div>
218 <div class="form-check">
219 <input class="form-check-input" type="checkbox" id="unlockAllScans" />
220 <label class="form-check-label" for="unlockAllScans">Unlock All Scans</label>
221 </div>
222 <div class="form-check">
223 <input class="form-check-input" type="checkbox" id="unlockAllMissions" />
224 <label class="form-check-label" for="unlockAllMissions">Unlock All Missions</label>
225 </div>
226 <div class="form-check">
227 <input class="form-check-input" type="checkbox" id="unlockAllQuests" />
228 <label class="form-check-label" for="unlockAllQuests">Unlock All Quests</label>
229 </div>
230 <div class="form-check">
231 <input class="form-check-input" type="checkbox" id="completeAllQuests" />
232 <label class="form-check-label" for="completeAllQuests">Complete All Quests</label>
233 </div>
234 <div class="form-check">
235 <input class="form-check-input" type="checkbox" id="infiniteCredits" />
236 <label class="form-check-label" for="infiniteCredits">Infinite Credits</label>
237 </div>
238 <div class="form-check">
239 <input class="form-check-input" type="checkbox" id="infinitePlatinum" />
240 <label class="form-check-label" for="infinitePlatinum">Infinite Platinum</label>
241 </div>
242 <div class="form-check">
243 <input class="form-check-input" type="checkbox" id="unlockAllShipFeatures" />
244 <label class="form-check-label" for="unlockAllShipFeatures">Unlock All Ship Features</label>
245 </div>
246 <div class="form-check">
247 <input class="form-check-input" type="checkbox" id="unlockAllShipDecorations" />
248 <label class="form-check-label" for="unlockAllShipDecorations">Unlock All Ship Decorations</label>
249 </div>
250 <div class="form-check">
251 <input class="form-check-input" type="checkbox" id="unlockAllFlavourItems" />
252 <label class="form-check-label" for="unlockAllFlavourItems">
253 Unlock All <abbr title="Animation Sets, Glyphs, Plattes, etc.">Flavor Items</abbr>
254 </label>
255 </div>
256 <div class="form-check">
257 <input class="form-check-input" type="checkbox" id="unlockAllSkins" />
258 <label class="form-check-label" for="unlockAllSkins">Unlock All Skins</label>
259 </div>
260 <div class="form-check">
261 <input class="form-check-input" type="checkbox" id="universalPolarityEverywhere" />
262 <label class="form-check-label" for="universalPolarityEverywhere">
263 Universal Polarity Everywhere
264 </label>
265 </div>
266 <div class="form-group mt-2">
267 <label class="form-label" for="spoofMasteryRank">
268 Spoofed Mastery Rank (-1 to disable)
269 </label>
270 <input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" />
271 </div>
272 <button class="btn btn-primary mt-3" type="submit">Save Settings</button>
273 </form>
274 </div>
270 275 </div>
271 276 </div>
272 277 <div class="col-lg-4">
Modified static/webui/script.js +29 -18
@@ -792,7 +792,7 @@ const uiConfigs = [
792 792 ];
793 793
794 794 function doChangeSettings() {
795 fetch("/custom/config")
795 fetch("/custom/config?" + window.authz)
796 796 .then(response => response.json())
797 797 .then(json => {
798 798 for (const i of uiConfigs) {
@@ -810,7 +810,7 @@ function doChangeSettings() {
810 810 }
811 811 }
812 812 $.post({
813 url: "/custom/config",
813 url: "/custom/config?" + window.authz,
814 814 contentType: "text/plain",
815 815 data: JSON.stringify(json, null, 2)
816 816 });
@@ -820,23 +820,34 @@ function doChangeSettings() {
820 820 // Cheats route
821 821
822 822 single.getRoute("/webui/cheats").on("beforeload", function () {
823 fetch("/custom/config")
824 .then(response => response.json())
825 .then(json =>
826 Object.entries(json).forEach(entry => {
827 const [key, value] = entry;
828 var x = document.getElementById(`${key}`);
829 if (x != null) {
830 if (x.type == "checkbox") {
831 if (value === true) {
832 x.setAttribute("checked", "checked");
833 }
834 } else if (x.type == "number") {
835 x.setAttribute("value", `${value}`);
836 }
823 let interval;
824 interval = setInterval(() => {
825 if (window.authz) {
826 clearInterval(interval);
827 fetch("/custom/config?" + window.authz).then(res => {
828 if (res.status == 200) {
829 $("#server-settings").removeClass("d-none");
830 res.json().then(json =>
831 Object.entries(json).forEach(entry => {
832 const [key, value] = entry;
833 var x = document.getElementById(`${key}`);
834 if (x != null) {
835 if (x.type == "checkbox") {
836 if (value === true) {
837 x.setAttribute("checked", "checked");
838 }
839 } else if (x.type == "number") {
840 x.setAttribute("value", `${value}`);
841 }
842 }
843 })
844 );
845 } else {
846 $("#server-settings-no-perms").removeClass("d-none");
837 847 }
838 })
839 );
848 });
849 }
850 }, 10);
840 851
841 852 fetch("http://localhost:61558/ping", { mode: "no-cors" })
842 853 .then(() => {