Compare commits

...

6 Commits

Author SHA1 Message Date
173a4791d9 Upload files to "static/webui" 2025-03-23 10:14:22 -07:00
8a29f06207 chore: use inventory projection for updateTheme (#1302)
Reviewed-on: OpenWF/SpaceNinjaServer#1302
2025-03-23 09:06:28 -07:00
cf3007b744 chore: update config when admin changes their name (#1298)
Reviewed-on: OpenWF/SpaceNinjaServer#1298
2025-03-23 09:06:08 -07:00
7f5592e00c chore: improve authentication and Dropped logic (#1296)
- Dropped is now also unset by getAccountForRequest
- Improved how nonce is validated to avoid possible parser mismatch issues to smuggle a 0
- Updated ircDroppedController to perform only a single MongoDB operation

Reviewed-on: OpenWF/SpaceNinjaServer#1296
2025-03-23 09:05:47 -07:00
c3d7ae33c2 chore: do 'git stash' before hard reset
Just in case the user made local changes and then runs the bat we don't wanna have it be irrecoverably lost.
2025-03-23 16:40:48 +01:00
aa12708738 chore: make addItem return InventoryChanges directly (#1299)
Reviewed-on: OpenWF/SpaceNinjaServer#1299
2025-03-23 08:26:46 -07:00
14 changed files with 68244 additions and 196 deletions

View File

@ -3,6 +3,7 @@
echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune
git stash
git reset --hard origin/main
if exist static\data\0\ (

View File

@ -118,7 +118,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
}
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false)).InventoryChanges
...(await addItem(inventory, recipe.resultType, recipe.num, false))
};
await inventory.save();
res.json({ InventoryChanges });

View File

@ -87,7 +87,7 @@ export const addStartingGear = async (
for (const item of awakeningRewards) {
const inventoryDelta = await addItem(inventory, item);
combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges);
combineInventoryChanges(inventoryChanges, inventoryDelta);
}
inventory.PlayedParkourTutorial = true;

View File

@ -194,7 +194,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
ItemCount: x.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
combineInventoryChanges(inventoryChanges, (await addItem(inventory, recipe.resultType)).InventoryChanges);
combineInventoryChanges(inventoryChanges, await addItem(inventory, recipe.resultType));
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges });

View File

@ -1,25 +1,23 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { updateTheme } from "@/src/services/inventoryService";
import { IThemeUpdateRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express";
import { getInventory } from "@/src/services/inventoryService";
const updateThemeController: RequestHandler = async (request, response) => {
export const updateThemeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
const body = String(request.body);
const data = getJSONfromString<IThemeUpdateRequest>(String(request.body));
try {
const json = getJSONfromString<IThemeUpdateRequest>(body);
if (typeof json !== "object") {
throw new Error("Invalid data format");
}
await updateTheme(json, accountId);
} catch (err) {
console.error("Error parsing JSON data:", err);
}
const inventory = await getInventory(accountId, "ThemeStyle ThemeBackground ThemeSounds");
if (data.Style) inventory.ThemeStyle = data.Style;
if (data.Background) inventory.ThemeBackground = data.Background;
if (data.Sounds) inventory.ThemeSounds = data.Sounds;
await inventory.save();
response.json({});
};
export { updateThemeController };
interface IThemeUpdateRequest {
Style?: string;
Background?: string;
Sounds?: string;
}

View File

@ -1,9 +1,24 @@
import { getAccountForRequest } from "@/src/services/loginService";
import { Account } from "@/src/models/loginModel";
import { RequestHandler } from "express";
export const ircDroppedController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
account.Dropped = true;
await account.save();
if (!req.query.accountId) {
throw new Error("Request is missing accountId parameter");
}
const nonce: number = parseInt(req.query.nonce as string);
if (!nonce) {
throw new Error("Request is missing nonce parameter");
}
await Account.updateOne(
{
_id: req.query.accountId,
Nonce: nonce
},
{
Dropped: true
}
);
res.end();
};

View File

@ -1,5 +1,6 @@
import { RequestHandler } from "express";
import { getAccountForRequest, isNameTaken } from "@/src/services/loginService";
import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService";
import { config, saveConfig } from "@/src/services/configService";
export const renameAccountController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
@ -7,8 +8,18 @@ export const renameAccountController: RequestHandler = async (req, res) => {
if (await isNameTaken(req.query.newname)) {
res.status(409).json("Name already in use");
} else {
if (isAdministrator(account)) {
for (let i = 0; i != config.administratorNames!.length; ++i) {
if (config.administratorNames![i] == account.DisplayName) {
config.administratorNames![i] = req.query.newname;
}
}
await saveConfig();
}
account.DisplayName = req.query.newname;
await account.save();
res.end();
}
} else {

View File

@ -19,9 +19,13 @@ import mongoose from "mongoose";
return "<BIGINT>" + this.toString() + "</BIGINT>";
};
const og_stringify = JSON.stringify;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
(JSON as any).stringify = (obj: any): string => {
return og_stringify(obj).split(`"<BIGINT>`).join(``).split(`</BIGINT>"`).join(``);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
JSON.stringify = (obj: any, replacer?: any, space?: string | number): string => {
return og_stringify(obj, replacer as string[], space)
.split(`"<BIGINT>`)
.join(``)
.split(`</BIGINT>"`)
.join(``);
};
}

View File

@ -34,7 +34,7 @@ interface IConfig {
httpsPort?: number;
myIrcAddresses?: string[];
NRS?: string[];
administratorNames?: string[] | string;
administratorNames?: string[];
autoCreateAccount?: boolean;
skipTutorial?: boolean;
skipAllDialogue?: boolean;
@ -83,10 +83,15 @@ export const updateConfig = async (data: string): Promise<void> => {
Object.assign(config, JSON.parse(data));
};
export const saveConfig = async (): Promise<void> => {
amnesia = true;
await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2));
};
export const validateConfig = (): void => {
if (typeof config.administratorNames == "string") {
logger.warn(
`"administratorNames" should be an array; please add square brackets: ["${config.administratorNames}"]`
);
logger.info(`Updating config.json to make administratorNames an array.`);
config.administratorNames = [config.administratorNames];
void saveConfig();
}
};

View File

@ -29,11 +29,7 @@ import {
ICrewShipWeaponClient
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
import {
IMissionInventoryUpdateRequest,
IThemeUpdateRequest,
IUpdateChallengeProgressRequest
} from "../types/requestTypes";
import { IMissionInventoryUpdateRequest, IUpdateChallengeProgressRequest } from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { convertInboxMessage, fromStoreItem, getExalted, getKeyChainItems } from "@/src/services/itemDataService";
import {
@ -234,10 +230,10 @@ export const addItem = async (
typeName: string,
quantity: number = 1,
premiumPurchase: boolean = false
): Promise<{ InventoryChanges: IInventoryChanges }> => {
): Promise<IInventoryChanges> => {
// Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments.
if (typeName in ExportBundles) {
return { InventoryChanges: await handleBundleAcqusition(typeName, inventory, quantity) };
return await handleBundleAcqusition(typeName, inventory, quantity);
}
// Strict typing
@ -250,9 +246,7 @@ export const addItem = async (
];
addRecipes(inventory, recipeChanges);
return {
InventoryChanges: {
Recipes: recipeChanges
}
Recipes: recipeChanges
};
}
if (typeName in ExportResources) {
@ -265,9 +259,7 @@ export const addItem = async (
];
addMiscItems(inventory, miscItemChanges);
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
MiscItems: miscItemChanges
};
} else if (ExportResources[typeName].productCategory == "FusionTreasures") {
const fusionTreasureChanges = [
@ -279,25 +271,21 @@ export const addItem = async (
];
addFusionTreasures(inventory, fusionTreasureChanges);
return {
InventoryChanges: {
FusionTreasures: fusionTreasureChanges
}
FusionTreasures: fusionTreasureChanges
};
} else if (ExportResources[typeName].productCategory == "Ships") {
const oid = await createShip(inventory.accountOwnerId, typeName);
inventory.Ships.push(oid);
return {
InventoryChanges: {
Ships: [
{
ItemId: { $oid: oid.toString() },
ItemType: typeName
}
]
}
Ships: [
{
ItemId: { $oid: oid.toString() },
ItemType: typeName
}
]
};
} else if (ExportResources[typeName].productCategory == "CrewShips") {
const inventoryChanges = {
return {
...addCrewShip(inventory, typeName),
// fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -305,8 +293,6 @@ export const addItem = async (
? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness")
: {})
};
return { InventoryChanges: inventoryChanges };
} else if (ExportResources[typeName].productCategory == "ShipDecorations") {
const changes = [
{
@ -316,9 +302,7 @@ export const addItem = async (
];
addShipDecorations(inventory, changes);
return {
InventoryChanges: {
ShipDecorations: changes
}
ShipDecorations: changes
};
} else if (ExportResources[typeName].productCategory == "KubrowPetEggs") {
const changes: IKubrowPetEggClient[] = [];
@ -339,9 +323,7 @@ export const addItem = async (
});
}
return {
InventoryChanges: {
KubrowPetEggs: changes
}
KubrowPetEggs: changes
};
} else {
throw new Error(`unknown product category: ${ExportResources[typeName].productCategory}`);
@ -349,14 +331,13 @@ export const addItem = async (
}
if (typeName in ExportCustoms) {
if (ExportCustoms[typeName].productCategory == "CrewShipWeaponSkins") {
return { InventoryChanges: addCrewShipWeaponSkin(inventory, typeName) };
return addCrewShipWeaponSkin(inventory, typeName);
} else {
return { InventoryChanges: addSkin(inventory, typeName) };
return addSkin(inventory, typeName);
}
}
if (typeName in ExportFlavour) {
const inventoryChanges = addCustomization(inventory, typeName);
return { InventoryChanges: inventoryChanges };
return addCustomization(inventory, typeName);
}
if (typeName in ExportUpgrades || typeName in ExportArcanes) {
const changes = [
@ -367,9 +348,7 @@ export const addItem = async (
];
addMods(inventory, changes);
return {
InventoryChanges: {
RawUpgrades: changes
}
RawUpgrades: changes
};
}
if (typeName in ExportGear) {
@ -381,9 +360,7 @@ export const addItem = async (
];
addConsumables(inventory, consumablesChanges);
return {
InventoryChanges: {
Consumables: consumablesChanges
}
Consumables: consumablesChanges
};
}
if (typeName in ExportWeapons) {
@ -426,14 +403,12 @@ export const addItem = async (
);
if (weapon.additionalItems) {
for (const item of weapon.additionalItems) {
combineInventoryChanges(inventoryChanges, (await addItem(inventory, item, 1)).InventoryChanges);
combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1));
}
}
return {
InventoryChanges: {
...inventoryChanges,
...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase)
}
...inventoryChanges,
...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase)
};
} else {
// Modular weapon parts
@ -445,36 +420,28 @@ export const addItem = async (
];
addMiscItems(inventory, miscItemChanges);
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
MiscItems: miscItemChanges
};
}
}
if (typeName in ExportRailjackWeapons) {
return {
InventoryChanges: {
...addCrewShipWeapon(inventory, typeName),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase)
}
...addCrewShipWeapon(inventory, typeName),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase)
};
}
if (typeName in ExportMisc.creditBundles) {
const creditsTotal = ExportMisc.creditBundles[typeName] * quantity;
inventory.RegularCredits += creditsTotal;
return {
InventoryChanges: {
RegularCredits: creditsTotal
}
RegularCredits: creditsTotal
};
}
if (typeName in ExportFusionBundles) {
const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity;
inventory.FusionPoints += fusionPointsTotal;
return {
InventoryChanges: {
FusionPoints: fusionPointsTotal
}
FusionPoints: fusionPointsTotal
};
}
if (typeName in ExportKeys) {
@ -483,8 +450,8 @@ export const addItem = async (
if (key.chainStages) {
const key = addQuestKey(inventory, { ItemType: typeName });
if (!key) return { InventoryChanges: {} };
return { InventoryChanges: { QuestKeys: [key] } };
if (!key) return {};
return { QuestKeys: [key] };
} else {
const key = { ItemType: typeName, ItemCount: quantity };
@ -494,19 +461,14 @@ export const addItem = async (
} else {
inventory.LevelKeys.push(key);
}
return { InventoryChanges: { LevelKeys: [key] } };
return { LevelKeys: [key] };
}
}
if (typeName in ExportDrones) {
const inventoryChanges = addDrone(inventory, typeName);
return {
InventoryChanges: inventoryChanges
};
return addDrone(inventory, typeName);
}
if (typeName in ExportEmailItems) {
return {
InventoryChanges: await addEmailItem(inventory, typeName)
};
return await addEmailItem(inventory, typeName);
}
// Path-based duck typing
@ -515,42 +477,36 @@ export const addItem = async (
switch (typeName.substr(1).split("/")[2]) {
default: {
return {
InventoryChanges: {
...addPowerSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase)
}
...addPowerSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase)
};
}
case "Archwing": {
inventory.ArchwingEnabled = true;
return {
InventoryChanges: {
...addSpaceSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase)
}
...addSpaceSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase)
};
}
case "EntratiMech": {
return {
InventoryChanges: {
...addMechSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase)
}
...addMechSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase)
};
}
}
@ -568,9 +524,7 @@ export const addItem = async (
];
addMods(inventory, changes);
return {
InventoryChanges: {
RawUpgrades: changes
}
RawUpgrades: changes
};
}
break;
@ -587,9 +541,7 @@ export const addItem = async (
];
addMiscItems(inventory, miscItemChanges);
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
MiscItems: miscItemChanges
};
} else {
const changes = [
@ -600,9 +552,7 @@ export const addItem = async (
];
addMods(inventory, changes);
return {
InventoryChanges: {
RawUpgrades: changes
}
RawUpgrades: changes
};
}
}
@ -613,9 +563,7 @@ export const addItem = async (
case "Types":
switch (typeName.substr(1).split("/")[2]) {
case "Sentinels": {
return {
InventoryChanges: addSentinel(inventory, typeName, premiumPurchase)
};
return addSentinel(inventory, typeName, premiumPurchase);
}
case "Game": {
if (typeName.substr(1).split("/")[3] == "Projections") {
@ -629,9 +577,7 @@ export const addItem = async (
addMiscItems(inventory, miscItemChanges);
inventory.HasOwnedVoidProjectionsPreviously = true;
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
MiscItems: miscItemChanges
};
}
break;
@ -639,27 +585,23 @@ export const addItem = async (
case "NeutralCreatures": {
const horseIndex = inventory.Horses.push({ ItemType: typeName });
return {
InventoryChanges: {
Horses: [inventory.Horses[horseIndex - 1].toJSON<IEquipmentClient>()]
}
Horses: [inventory.Horses[horseIndex - 1].toJSON<IEquipmentClient>()]
};
}
case "Recipes": {
inventory.MiscItems.push({ ItemType: typeName, ItemCount: quantity });
return {
InventoryChanges: {
MiscItems: [
{
ItemType: typeName,
ItemCount: quantity
}
]
}
MiscItems: [
{
ItemType: typeName,
ItemCount: quantity
}
]
};
}
case "Vehicles":
if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") {
return { InventoryChanges: addMotorcycle(inventory, typeName) };
return addMotorcycle(inventory, typeName);
}
break;
}
@ -680,7 +622,7 @@ export const addItems = async (
} else {
inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount, true);
}
combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges);
combineInventoryChanges(inventoryChanges, inventoryDelta);
}
return inventoryChanges;
};
@ -945,15 +887,6 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr
};
};
export const updateTheme = async (data: IThemeUpdateRequest, accountId: string): Promise<void> => {
const inventory = await getInventory(accountId);
if (data.Style) inventory.ThemeStyle = data.Style;
if (data.Background) inventory.ThemeBackground = data.Background;
if (data.Sounds) inventory.ThemeSounds = data.Sounds;
await inventory.save();
};
export const addEquipment = (
inventory: TInventoryDatabaseDocument,
category: TEquipmentKey,
@ -1388,12 +1321,11 @@ export const addKeyChainItems = async (
const nonStoreItems = keyChainItems.map(item => fromStoreItem(item));
//TODO: inventoryChanges is not typed correctly
const inventoryChanges = {};
const inventoryChanges: IInventoryChanges = {};
for (const item of nonStoreItems) {
const inventoryChangesDelta = await addItem(inventory, item);
combineInventoryChanges(inventoryChanges, inventoryChangesDelta.InventoryChanges);
combineInventoryChanges(inventoryChanges, inventoryChangesDelta);
}
return inventoryChanges;

View File

@ -69,36 +69,31 @@ export const getAccountForRequest = async (req: Request): Promise<TAccountDocume
if (!req.query.accountId) {
throw new Error("Request is missing accountId parameter");
}
if (!req.query.nonce || parseInt(req.query.nonce as string) === 0) {
const nonce: number = parseInt(req.query.nonce as string);
if (!nonce) {
throw new Error("Request is missing nonce parameter");
}
const account = await Account.findOne({
_id: req.query.accountId,
Nonce: req.query.nonce
Nonce: nonce
});
if (!account) {
throw new Error("Invalid accountId-nonce pair");
}
if (account.Dropped && req.query.ct) {
account.Dropped = undefined;
await account.save();
}
return account;
};
export const getAccountIdForRequest = async (req: Request): Promise<string> => {
const account = await getAccountForRequest(req);
if (account.Dropped && req.query.ct) {
account.Dropped = undefined;
await account.save();
}
return account._id.toString();
return (await getAccountForRequest(req))._id.toString();
};
export const isAdministrator = (account: TAccountDocument): boolean => {
if (!config.administratorNames) {
return false;
}
if (typeof config.administratorNames == "string") {
return config.administratorNames == account.DisplayName;
}
return !!config.administratorNames.find(x => x == account.DisplayName);
return !!config.administratorNames?.find(x => x == account.DisplayName);
};
const platform_magics = [753, 639, 247, 37, 60];

View File

@ -333,7 +333,7 @@ export const handleStoreItemAcquisition = async (
}
switch (storeCategory) {
default: {
purchaseResponse = await addItem(inventory, internalName, quantity, true);
purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true) };
break;
}
case "Types":
@ -418,10 +418,7 @@ const handleBoosterPackPurchase = async (
if (typeName == "/Lotus/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed") {
for (const result of pack.components) {
purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
combineInventoryChanges(
purchaseResponse.InventoryChanges,
(await addItem(inventory, result.Item, 1)).InventoryChanges
);
combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1));
}
} else {
for (let i = 0; i != quantity; ++i) {
@ -432,7 +429,7 @@ const handleBoosterPackPurchase = async (
purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
combineInventoryChanges(
purchaseResponse.InventoryChanges,
(await addItem(inventory, result.Item, 1)).InventoryChanges
await addItem(inventory, result.Item, 1)
);
}
}
@ -468,7 +465,7 @@ const handleTypesPurchase = async (
logger.debug(`type category ${typeCategory}`);
switch (typeCategory) {
default:
return await addItem(inventory, typesName, quantity);
return { InventoryChanges: await addItem(inventory, typesName, quantity) };
case "BoosterPacks":
return handleBoosterPackPurchase(typesName, inventory, quantity);
case "SlotItems":

View File

@ -19,12 +19,6 @@ import {
ICollectibleEntry
} from "./inventoryTypes/inventoryTypes";
export interface IThemeUpdateRequest {
Style?: string;
Background?: string;
Sounds?: string;
}
export interface IAffiliationChange {
Tag: string;
Standing: number;

68096
static/webui/inventory.json Normal file

File diff suppressed because it is too large Load Diff