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

SpaceNinjaServer

A simple server for a small space ninja game

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

XFEstudio/SpaceNinjaServer

feat(webui): possession (#3739)

Adds a new tab to the webui for admins to see registered losers with the option to possess them Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3739 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>

c59a097a
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

15 个文件 +227 -46
Added src/controllers/custom/getRegisteredLosersController.ts +12 -0
@@ -0,0 +1,12 @@
1 import type { RequestHandler } from "express";
2 import { getAccountForRequest, isAdministrator } from "../../services/loginService.ts";
3 import { Account } from "../../models/loginModel.ts";
4
5 export const getRegisteredLosersController: RequestHandler = async (req, res) => {
6 const account = await getAccountForRequest(req);
7 if (isAdministrator(account)) {
8 res.json(await Account.find({}, "id DisplayName"));
9 } else {
10 res.status(401).end();
11 }
12 };
Modified src/routes/custom.ts +2 -0
@@ -30,6 +30,7 @@ import { removeIsNewController } from "../controllers/custom/removeIsNewControll
30 30 import { removeItemsController } from "../controllers/custom/removeItemsController.ts";
31 31 import { retroactivelyApplyCheatController } from "../controllers/api/retroactivelyApplyCheatController.ts";
32 32 import { retroactivelyApplyGuildCheatController } from "../controllers/custom/retroactivelyApplyGuildCheatController.ts";
33 import { getRegisteredLosersController } from "../controllers/custom/getRegisteredLosersController.ts";
33 34
34 35 import { abilityOverrideController } from "../controllers/custom/abilityOverrideController.ts";
35 36 import { createAccountController } from "../controllers/custom/createAccountController.ts";
@@ -88,6 +89,7 @@ customRouter.get("/removeIsNew", removeIsNewController);
88 89 customRouter.get("/removeItems", removeItemsController);
89 90 customRouter.get("/retroactivelyApplyCheat", retroactivelyApplyCheatController);
90 91 customRouter.get("/retroactivelyApplyGuildCheat", retroactivelyApplyGuildCheatController);
92 customRouter.get("/getRegisteredLosers", getRegisteredLosersController);
91 93
92 94 customRouter.post("/abilityOverride", abilityOverrideController);
93 95 customRouter.post("/createAccount", createAccountController);
Modified src/routes/webui.ts +1 -0
@@ -43,6 +43,7 @@ webuiRouter.get("/webui/quests", virtualRouteController);
43 43 webuiRouter.get("/webui/cheats", virtualRouteController);
44 44 webuiRouter.get("/webui/import", virtualRouteController);
45 45 webuiRouter.get("/webui/guildView", virtualRouteController);
46 webuiRouter.get("/webui/admin", virtualRouteController);
46 47
47 48 // Serve static files
48 49 webuiRouter.use("/webui", express.static(path.join(baseDir, "static/webui")));
Modified src/services/loginService.ts +15 -5
@@ -105,13 +105,23 @@ export const getAccountForQuery = async (
105 105 throw new Error("Request is missing nonce parameter");
106 106 }
107 107 const account = await Account.findById(query.accountId);
108 if (!account || account.Nonce != nonce) {
108 if (!account) {
109 109 throw new Error("Invalid accountId-nonce pair");
110 110 }
111 if (account.Dropped && query.ct) {
112 logger.debug(`removing dropped mark from ${query.accountId}`);
113 account.Dropped = undefined;
114 await account.save();
111 if (query.possesser) {
112 const possesser = await Account.findOne({ _id: query.possesser, Nonce: nonce });
113 if (!possesser || !isAdministrator(possesser)) {
114 throw new Error(`Invalid accountId-nonce pair`);
115 }
116 } else {
117 if (account.Nonce != nonce) {
118 throw new Error("Invalid accountId-nonce pair");
119 }
120 if (account.Dropped && query.ct) {
121 logger.debug(`removing dropped mark from ${query.accountId}`);
122 account.Dropped = undefined;
123 await account.save();
124 }
115 125 }
116 126 return account;
117 127 };
Modified src/services/wsService.ts +15 -5
@@ -62,6 +62,7 @@ interface IWsCustomData extends WebSocket {
62 62 address: string;
63 63 reflexiveAddress: string;
64 64 accountId?: string;
65 realAccountId?: string;
65 66 isGame?: boolean;
66 67 }
67 68
@@ -69,6 +70,7 @@ interface IWsMsgFromClient {
69 70 auth?: {
70 71 email: string;
71 72 password: string;
73 possessing?: string;
72 74 isRegister: boolean;
73 75 };
74 76 auth_game?:
@@ -135,6 +137,7 @@ const wsOnConnect = (ws: WebSocket, req: http.IncomingMessage): void => {
135 137 const data = JSON.parse(String(msg)) as IWsMsgFromClient;
136 138 if (data.auth) {
137 139 let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
140 let accessedAccount = account;
138 141 if (account) {
139 142 if (isCorrectPassword(data.auth.password, account.password)) {
140 143 if (!account.Nonce) {
@@ -142,6 +145,11 @@ const wsOnConnect = (ws: WebSocket, req: http.IncomingMessage): void => {
142 145 account.Nonce = createNonce();
143 146 await (account as HydratedDocument<IDatabaseAccountJson>).save();
144 147 }
148 if (data.auth.possessing) {
149 accessedAccount = isAdministrator(account)
150 ? await Account.findById(data.auth.possessing)
151 : null;
152 }
145 153 } else {
146 154 account = null;
147 155 }
@@ -155,18 +163,20 @@ const wsOnConnect = (ws: WebSocket, req: http.IncomingMessage): void => {
155 163 DisplayName: name,
156 164 Nonce: createNonce()
157 165 });
166 accessedAccount = account;
158 167 }
159 if (account) {
160 (ws as IWsCustomData).accountId = account.id;
168 if (account && accessedAccount) {
169 (ws as IWsCustomData).accountId = accessedAccount.id;
170 (ws as IWsCustomData).realAccountId = account.id;
161 171 if (!config.webui?.adminOnly || isAdministrator(account)) {
162 172 ws.send(
163 173 JSON.stringify({
164 174 auth_succ: {
165 175 id: account.id,
166 DisplayName: account.DisplayName,
176 DisplayName: accessedAccount.DisplayName,
167 177 Nonce: account.Nonce
168 178 },
169 have_game_ws: haveGameWs(account.id)
179 have_game_ws: haveGameWs(accessedAccount.id)
170 180 } satisfies IWsMsgToClient)
171 181 );
172 182 } else {
@@ -339,7 +349,7 @@ export const handleNonceInvalidation = (accountId: string): void => {
339 349 export const bootNonAdminsFromWebui = (): void => {
340 350 forEachWsClient(client => {
341 351 if (client.accountId && !client.isGame) {
342 void Account.findById(client.accountId).then(account => {
352 void Account.findById(client.realAccountId ?? client.accountId).then(account => {
343 353 if (!account || !isAdministrator(account)) {
344 354 client.send(JSON.stringify({ logged_out: true } satisfies IWsMsgToClientWebui));
345 355 client.close();
Modified static/webui/index.html +23 -5
@@ -39,7 +39,7 @@
39 39 <div class="nav-item dropdown user-dropdown">
40 40 <button class="nav-link dropdown-toggle displayname" data-bs-toggle="dropdown" aria-expanded="false"></button>
41 41 <ul class="dropdown-menu dropdown-menu-end">
42 <li><a class="dropdown-item" href="/webui/" onclick="doLogout();" data-loc="navbar_logout"></a></li>
42 <li><a class="dropdown-item" href="#" onclick="event.preventDefault();doLogout();" data-loc="navbar_logout"></a></li>
43 43 <li><hr class="dropdown-divider"></li>
44 44 <li><a class="dropdown-item" href="#" onclick="event.preventDefault();changeAccountPassword();" data-loc="navbar_changePassword"></a></li>
45 45 <li><a class="dropdown-item" href="#" onclick="event.preventDefault();changeAccountEmail();" data-loc="navbar_changeEmail"></a></li>
@@ -73,9 +73,12 @@
73 73 <li class="nav-item">
74 74 <a class="nav-link" href="/webui/import" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" data-loc="navbar_import"></a>
75 75 </li>
76 <li class="nav-item" id="nav-guildView">
76 <li class="nav-item">
77 77 <a class="nav-link" href="/webui/guildView" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" data-loc="navbar_guildView"></a>
78 78 </li>
79 <li class="nav-item">
80 <a class="nav-link" href="/webui/admin" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" data-loc="navbar_admin"></a>
81 </li>
79 82 </ul>
80 83 </div>
81 84 </div>
@@ -1200,10 +1203,10 @@
1200 1203 <div class="card mb-3">
1201 1204 <h5 class="card-header" data-loc="cheats_server"></h5>
1202 1205 <div class="card-body">
1203 <div class="d-none config-admin-hide">
1206 <div class="d-none admin-hide">
1204 1207 <p class="card-text" data-loc="cheats_administratorRequirement"></p>
1205 1208 </div>
1206 <div class="d-none config-admin-show config-form">
1209 <div class="d-none admin-show config-form">
1207 1210 <div class="form-check">
1208 1211 <input class="form-check-input" type="checkbox" id="skipTutorial" />
1209 1212 <label class="form-check-label" for="skipTutorial" data-loc="cheats_skipTutorial"></label>
@@ -1223,7 +1226,7 @@
1223 1226 </div>
1224 1227 </div>
1225 1228 </div>
1226 <div class="card d-none config-admin-show config-form">
1229 <div class="card d-none admin-show config-form">
1227 1230 <h5 class="card-header" data-loc="worldState"></h5>
1228 1231 <div class="card-body">
1229 1232 <div class="form-check">
@@ -1705,6 +1708,21 @@
1705 1708 <li><a href="#" onclick="event.preventDefault();setImportSample('eidolonLocPins');" data-loc="import_samples_eidolonLocPins"></a></li>
1706 1709 </ul>
1707 1710 </div>
1711 <div data-route="/webui/admin" data-title-tag="navbar_admin">
1712 <div class="d-none admin-hide">
1713 <p class="card-text" data-loc="cheats_administratorRequirement"></p>
1714 </div>
1715 <div class="d-none admin-show">
1716 <div class="card">
1717 <h5 class="card-header" data-loc="admin_users"></h5>
1718 <div class="card-body">
1719 <table class="table table-small table-hover mb-0">
1720 <tbody id="registered-losers"></tbody>
1721 </table>
1722 </div>
1723 </div>
1724 </div>
1725 </div>
1708 1726 </div>
1709 1727 <div class="toast-container position-fixed bottom-0 end-0 p-3"></div>
1710 1728 </div>
Modified static/webui/script.js +120 -31
@@ -21,6 +21,7 @@ const sendAuth = isRegister => {
21 21 auth: {
22 22 email: localStorage.getItem("email").toLowerCase(),
23 23 password: wp.encSync(localStorage.getItem("password")),
24 possessing: localStorage.getItem("possessing"),
24 25 isRegister
25 26 }
26 27 })
@@ -65,7 +66,17 @@ function openWebSocket() {
65 66 $("#password").val("");
66 67 $(".displayname").text(data.DisplayName);
67 68 window.accountId = data.id;
68 window.authz = "accountId=" + data.id + "&nonce=" + data.Nonce + "&wsid=" + wsid;
69 window.authz = "accountId=";
70 let possessing = localStorage.getItem("possessing");
71 if (possessing == data.id) {
72 localStorage.removeItem("possessing");
73 possessing = null;
74 }
75 if (possessing) {
76 window.accountId = possessing;
77 window.authz += possessing + "&possesser=";
78 }
79 window.authz += data.id + "&nonce=" + data.Nonce + "&wsid=" + wsid;
69 80 if (window.dict) {
70 81 updateLocElements();
71 82 }
@@ -81,21 +92,26 @@ function openWebSocket() {
81 92 }
82 93 if ("auth_fail" in msg) {
83 94 auth_pending = false;
84 logout();
85 if (single.getCurrentPath() == "/webui/") {
86 if (msg.auth_fail == "bad login") {
87 alert(loc("code_loginFail"));
88 } else if (msg.auth_fail == "bad register") {
89 alert(loc("code_regFail"));
90 } else if (msg.auth_fail == "admin only") {
91 alert(loc("code_adminOnlyLogin"));
92 } else if (msg.auth_fail == "registered but admin only") {
93 alert(loc("code_adminOnlyRegister"));
95 if (localStorage.getItem("possessing")) {
96 localStorage.removeItem("possessing");
97 sendAuth();
98 } else {
99 logout();
100 if (single.getCurrentPath() == "/webui/") {
101 if (msg.auth_fail == "bad login") {
102 alert(loc("code_loginFail"));
103 } else if (msg.auth_fail == "bad register") {
104 alert(loc("code_regFail"));
105 } else if (msg.auth_fail == "admin only") {
106 alert(loc("code_adminOnlyLogin"));
107 } else if (msg.auth_fail == "registered but admin only") {
108 alert(loc("code_adminOnlyRegister"));
109 } else {
110 alert(msg.auth_fail);
111 }
94 112 } else {
95 alert(msg.auth_fail);
113 single.loadRoute("/webui/");
96 114 }
97 } else {
98 single.loadRoute("/webui/");
99 115 }
100 116 }
101 117 if ("nonce_updated" in msg) {
@@ -107,8 +123,13 @@ function openWebSocket() {
107 123 updateInventory();
108 124 }
109 125 if ("logged_out" in msg) {
110 logout();
111 single.loadRoute("/webui/"); // Show login screen
126 if (localStorage.getItem("possessing")) {
127 localStorage.removeItem("possessing");
128 sendAuth();
129 } else {
130 logout();
131 single.loadRoute("/webui/"); // Show login screen
132 }
112 133 }
113 134 if ("have_game_ws" in msg) {
114 135 window.have_game_ws = msg.have_game_ws;
@@ -129,8 +150,8 @@ openWebSocket();
129 150
130 151 function refreshServerConfig() {
131 152 //window.is_admin = undefined;
132 if (single.getCurrentPath() == "/webui/cheats") {
133 single.loadRoute("/webui/cheats");
153 if (single.getCurrentPath() == "/webui/cheats" || single.getCurrentPath() == "/webui/admin") {
154 single.loadRoute(single.getCurrentPath());
134 155 }
135 156 }
136 157
@@ -177,10 +198,16 @@ function logout() {
177 198 }
178 199
179 200 function doLogout() {
180 logout();
181 if (ws_is_open) {
182 // Unsubscribe from notifications about nonce invalidation
183 window.ws.send(JSON.stringify({ logout: true }));
201 if (localStorage.getItem("possessing")) {
202 localStorage.removeItem("possessing");
203 location.href = "/webui/inventory";
204 } else {
205 logout();
206 if (ws_is_open) {
207 // Unsubscribe from notifications about nonce invalidation
208 window.ws.send(JSON.stringify({ logout: true }));
209 }
210 location.href = "/webui/";
184 211 }
185 212 }
186 213
@@ -206,12 +233,13 @@ function renameAccount(taken_name) {
206 233 }
207 234
208 235 function deleteAccount() {
209 if (window.confirm(loc("code_deleteAccountConfirm"))) {
236 if (
237 window.confirm(
238 loc(localStorage.getItem("possessing") ? "code_deletePosssedAccountConfirm" : "code_deleteAccountConfirm")
239 )
240 ) {
210 241 revalidateAuthz().then(() => {
211 fetch("/custom/deleteAccount?" + window.authz).then(() => {
212 logout();
213 single.loadRoute("/webui/"); // Show login screen
214 });
242 fetch("/custom/deleteAccount?" + window.authz);
215 243 });
216 244 }
217 245 }
@@ -3940,8 +3968,8 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
3940 3968 })
3941 3969 .done(json => {
3942 3970 //window.is_admin = true;
3943 $(".config-admin-hide").addClass("d-none");
3944 $(".config-admin-show").removeClass("d-none");
3971 $(".admin-hide").addClass("d-none");
3972 $(".admin-show").removeClass("d-none");
3945 3973 Object.entries(json).forEach(entry => {
3946 3974 const [key, value] = entry;
3947 3975 const elm = document.getElementById(key);
@@ -3968,8 +3996,8 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
3968 3996 });
3969 3997 } else {
3970 3998 //window.is_admin = false;
3971 $(".config-admin-hide").removeClass("d-none");
3972 $(".config-admin-show").addClass("d-none");
3999 $(".admin-hide").removeClass("d-none");
4000 $(".admin-show").addClass("d-none");
3973 4001 }
3974 4002 });
3975 4003 }
@@ -5150,3 +5178,64 @@ function removeItems(category) {
5150 5178 }
5151 5179 });
5152 5180 }
5181
5182 single.getRoute("/webui/admin").on("beforeload", function () {
5183 let interval;
5184 interval = setInterval(() => {
5185 if (window.authz) {
5186 clearInterval(interval);
5187 $.get("/custom/getRegisteredLosers?" + window.authz)
5188 .done(users => {
5189 //window.is_admin = true;
5190 $(".admin-hide").addClass("d-none");
5191 $(".admin-show").removeClass("d-none");
5192 document.getElementById("registered-losers").innerHTML = "";
5193 for (const user of users) {
5194 const tr = document.createElement("tr");
5195 {
5196 const td = document.createElement("td");
5197 td.textContent = user.DisplayName;
5198 tr.appendChild(td);
5199 }
5200 {
5201 const td = document.createElement("td");
5202 td.innerHTML = `<code>${user.id}</code>`;
5203 tr.appendChild(td);
5204 }
5205 {
5206 const td = document.createElement("td");
5207 if (window.accountId != user.id) {
5208 const a = document.createElement("a");
5209 a.textContent = loc(`admin_possess`);
5210 a.href = "#";
5211 a.onclick = function () {
5212 localStorage.setItem("possessing", user.id);
5213 location.href = "/webui/inventory";
5214 };
5215 td.appendChild(a);
5216 }
5217 tr.appendChild(td);
5218 }
5219 document.getElementById("registered-losers").appendChild(tr);
5220 }
5221 })
5222 .fail(res => {
5223 if (res.responseText == "Log-in expired") {
5224 if (ws_is_open && !auth_pending) {
5225 console.warn("Credentials invalidated but the server didn't let us know");
5226 sendAuth();
5227 }
5228 revalidateAuthz().then(() => {
5229 if (single.getCurrentPath() == "/webui/admin") {
5230 single.loadRoute("/webui/admin");
5231 }
5232 });
5233 } else {
5234 //window.is_admin = false;
5235 $(".admin-hide").removeClass("d-none");
5236 $(".admin-show").addClass("d-none");
5237 }
5238 });
5239 }
5240 }, 10);
5241 });
Modified static/webui/style.css +4 -0
@@ -29,6 +29,10 @@ body:not(.logged-in) .user-dropdown {
29 29 display: none;
30 30 }
31 31
32 .card-body .table.mb-0 tr:last-child {
33 border-bottom-color: transparent;
34 }
35
32 36 /* font awesome icons */
33 37 svg {
34 38 fill: currentColor;
Modified static/webui/translations/de.js +5 -0
@@ -16,6 +16,7 @@ dict = {
16 16 code_changeNameConfirm: `In welchen Namen möchtest du deinen Account umbenennen?`,
17 17 code_changeNameRetry: `|NAME| ist bereits vergeben.`,
18 18 code_deleteAccountConfirm: `Bist du sicher, dass du deinen Account |DISPLAYNAME| (|EMAIL|) löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.`,
19 code_deletePosssedAccountConfirm: `[UNTRANSLATED] Are you sure you want to delete the account |DISPLAYNAME|? This action cannot be undone.`,
19 20 code_archgun: `Arch-Gewehr`,
20 21 code_melee: `Nahkampf`,
21 22 code_pistol: `Pistole`,
@@ -530,5 +531,9 @@ dict = {
530 531 guildView_promote: `Befördern`,
531 532 guildView_demote: `Degradieren`,
532 533
534 navbar_admin: `[UNTRANSLATED] Admin`,
535 admin_users: `[UNTRANSLATED] Users`,
536 admin_possess: `[UNTRANSLATED] Possess`,
537
533 538 prettier_sucks_ass: ``
534 539 };
Modified static/webui/translations/en.js +5 -0
@@ -15,6 +15,7 @@ dict = {
15 15 code_changeNameConfirm: `What would you like to change your account name to?`,
16 16 code_changeNameRetry: `|NAME| is already taken.`,
17 17 code_deleteAccountConfirm: `Are you sure you want to delete your account |DISPLAYNAME| (|EMAIL|)? This action cannot be undone.`,
18 code_deletePosssedAccountConfirm: `Are you sure you want to delete the account |DISPLAYNAME|? This action cannot be undone.`,
18 19 code_archgun: `Archgun`,
19 20 code_melee: `Melee`,
20 21 code_pistol: `Pistol`,
@@ -529,5 +530,9 @@ dict = {
529 530 guildView_promote: `Promote`,
530 531 guildView_demote: `Demote`,
531 532
533 navbar_admin: `Admin`,
534 admin_users: `Users`,
535 admin_possess: `Possess`,
536
532 537 prettier_sucks_ass: ``
533 538 };
Modified static/webui/translations/es.js +5 -0
@@ -16,6 +16,7 @@ dict = {
16 16 code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`,
17 17 code_changeNameRetry: `|NAME| Ya está en uso.`,
18 18 code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`,
19 code_deletePosssedAccountConfirm: `[UNTRANSLATED] Are you sure you want to delete the account |DISPLAYNAME|? This action cannot be undone.`,
19 20 code_archgun: `Archcañón`,
20 21 code_melee: `Cuerpo a cuerpo`,
21 22 code_pistol: `Pistola`,
@@ -530,5 +531,9 @@ dict = {
530 531 guildView_promote: `Promover`,
531 532 guildView_demote: `Degradar`,
532 533
534 navbar_admin: `[UNTRANSLATED] Admin`,
535 admin_users: `[UNTRANSLATED] Users`,
536 admin_possess: `[UNTRANSLATED] Possess`,
537
533 538 prettier_sucks_ass: ``
534 539 };
Modified static/webui/translations/fr.js +5 -0
@@ -16,6 +16,7 @@ dict = {
16 16 code_changeNameConfirm: `Nouveau nom du compte :`,
17 17 code_changeNameRetry: `|NAME| est déjà pris.`,
18 18 code_deleteAccountConfirm: `Supprimer |DISPLAYNAME| (|EMAIL|) ? Cette action est irreversible.`,
19 code_deletePosssedAccountConfirm: `[UNTRANSLATED] Are you sure you want to delete the account |DISPLAYNAME|? This action cannot be undone.`,
19 20 code_archgun: `Archgun`,
20 21 code_melee: `Melee`,
21 22 code_pistol: `Pistolet`,
@@ -530,5 +531,9 @@ dict = {
530 531 guildView_promote: `Promouvoir`,
531 532 guildView_demote: `Rétrograder`,
532 533
534 navbar_admin: `[UNTRANSLATED] Admin`,
535 admin_users: `[UNTRANSLATED] Users`,
536 admin_possess: `[UNTRANSLATED] Possess`,
537
533 538 prettier_sucks_ass: ``
534 539 };
Modified static/webui/translations/ru.js +5 -0
Modified static/webui/translations/uk.js +5 -0
Modified static/webui/translations/zh.js +5 -0