返回提交历史
Modified
config.json.example
+1
-0
Modified
src/controllers/api/loginController.ts
+11
-2
Modified
src/controllers/custom/createAccountController.ts
+8
-6
Modified
src/controllers/custom/getConfigDataController.ts
+8
-2
Modified
src/controllers/custom/renameAccountController.ts
+8
-4
Modified
src/controllers/custom/updateConfigDataController.ts
+8
-2
Modified
src/models/loginModel.ts
+1
-1
Modified
src/services/configService.ts
+8
-0
Modified
src/services/loginService.ts
+19
-9
Modified
static/webui/index.html
+73
-68
Modified
static/webui/script.js
+29
-18
XFEstudio/XFESpaceNinjaServer
feat: add administrators, require administrator perms to change server config in webui (#628)
103e9bc4
代码差异
11 个文件
+174
-112
@@ -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,
@@ -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,
@@ -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 };
@@ -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 };
@@ -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
}
@@ -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 };
@@ -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 },
@@ -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;
@@ -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
};
@@ -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">
@@ -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(() => {