From b2425f1ab6b4bbec126e755f00a8845c63b2226e Mon Sep 17 00:00:00 2001 From: Lawin0129 <56766256+Lawin0129@users.noreply.github.com> Date: Sun, 23 Apr 2023 13:59:43 +0100 Subject: [PATCH] New command, MCP code updates and revert createError change --- DiscordBot/commands/change-username.js | 3 + .../commands/sign-out-of-all-sessions.js | 27 +++ README.md | 32 +-- matchmaker/matchmaker.js | 25 +-- routes/auth.js | 44 ++--- routes/mcp.js | 185 +++++++++--------- routes/user.js | 16 +- structs/error.js | 11 +- structs/profile.js | 3 +- tokenManager/tokenVerify.js | 24 +-- 10 files changed, 210 insertions(+), 160 deletions(-) create mode 100644 DiscordBot/commands/sign-out-of-all-sessions.js diff --git a/DiscordBot/commands/change-username.js b/DiscordBot/commands/change-username.js index f0f5053..a495f27 100644 --- a/DiscordBot/commands/change-username.js +++ b/DiscordBot/commands/change-username.js @@ -19,6 +19,9 @@ module.exports = { const user = await User.findOne({ discordId: interaction.user.id }); if (!user) return interaction.editReply({ content: "You do not have a registered account!", ephemeral: true }); + let accessToken = global.accessTokens.find(i => i.accountId == user.accountId); + if (accessToken) return interaction.editReply({ content: "Failed to change username as you are currently logged in to Fortnite.\nRun the /sign-out-of-all-sessions command to sign out.", ephemeral: true }); + const { options } = interaction; let username = options.get("username").value; diff --git a/DiscordBot/commands/sign-out-of-all-sessions.js b/DiscordBot/commands/sign-out-of-all-sessions.js new file mode 100644 index 0000000..55f9e3a --- /dev/null +++ b/DiscordBot/commands/sign-out-of-all-sessions.js @@ -0,0 +1,27 @@ +const User = require("../../model/user.js"); + +module.exports = { + commandInfo: { + name: "sign-out-of-all-sessions", + description: "Signs you out if you have an active session." + }, + execute: async (interaction) => { + await interaction.deferReply({ ephemeral: true }); + + const targetUser = await User.findOne({ discordId: interaction.user.id }).lean(); + if (!targetUser) return interaction.editReply({ content: "You do not have a registered account!", ephemeral: true }); + + let accessToken = global.accessTokens.findIndex(i => i.accountId == targetUser.accountId); + if (accessToken != -1) global.accessTokens.splice(accessToken, 1); + + let refreshToken = global.refreshTokens.findIndex(i => i.accountId == targetUser.accountId); + if (refreshToken != -1) global.refreshTokens.splice(refreshToken, 1); + + let xmppClient = global.Clients.find(client => client.accountId == targetUser.accountId); + if (xmppClient) xmppClient.client.close(); + + if (accessToken != -1 || refreshToken != -1) return interaction.editReply({ content: `Successfully signed out of all sessions!`, ephemeral: true }); + + interaction.editReply({ content: `You have no current active sessions.`, ephemeral: true }); + } +} \ No newline at end of file diff --git a/README.md b/README.md index bb14be6..0a600b3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,24 @@ # LawinServerV2 -### LawinServer V2 is a fortnite backend that features an account system and xmpp. +### LawinServer V2 is a fortnite backend written in Node.js that features an account system and xmpp. ## Features ### LawinServer V2 -- CloudStorage and ClientSettings (Settings Saving) -- Changing items in locker -- Changing banner icon and banner color -- Changing item edit styles -- Favoriting items -- Marking items as seen -- Managing friends (e.g. adding friends) +* CloudStorage and ClientSettings (Settings Saving). +* Locker: + + Changing items. + + Changing banner icon and banner color. + + Changing item edit styles. + + Favoriting items. + + Marking items as seen. +* Friends: + + Adding friends. + + Accepting friend requests. + + Removing friends. + + Blocking friends. ### XMPP Features -- Parties (builds 3.5 to 14.50) -- Chat (whispering, global chat, party chat) -- Friends +- Parties (builds 3.5 to 14.50). +- Chat (whispering, global chat, party chat). +- Friends. ### NOTE: LawinServerV2 does not support Save the World. ## Discord Bot Commands @@ -23,6 +28,7 @@ - `/exchange-code` - Generates an exchange code for login. (One time use for each code and if not used it expires after 5 mins). - `/change-username {newUsername}` - You can change your username using this command. - `/change-password {newPassword}` - You can change your password using this command. +- `/sign-out-of-all-sessions` - Signs you out if you have an active session. ### Moderation Commands: - You can only use the moderation commands if you are a moderator. - `/ban {targetUsername}` - Ban a user from the backend by their username. @@ -32,10 +38,10 @@ 1) Go to Config/config.json in the directory you extracted LawinServerV2 into. 2) Open it, you should see a "moderators" section in the file. 3) You have to get your discord id and replace discordId with it. -4) You can set multiple moderators like this `["discordId","discordId2"]` +4) You can set multiple moderators like this `["discordId","discordId2"]`. ## Hosting for others -You are allowed to host for others, however please don't remove my credits from `responses/contentpages.json` +You are allowed to host for others, however please credit me and don't remove my credits from `responses/contentpages.json`. ## How to host LawinServerV2 1) Install [NodeJS](https://nodejs.org/en/) and [MongoDB](https://www.mongodb.com/try/download/community). diff --git a/matchmaker/matchmaker.js b/matchmaker/matchmaker.js index 77eeef4..344726e 100644 --- a/matchmaker/matchmaker.js +++ b/matchmaker/matchmaker.js @@ -1,17 +1,20 @@ -const crypto = require("crypto"); +const functions = require("../structs/functions.js"); -module.exports = (ws) => { +module.exports = async (ws) => { // create hashes - const ticketId = crypto.createHash('md5').update(`1${Date.now()}`).digest('hex'); - const matchId = crypto.createHash('md5').update(`2${Date.now()}`).digest('hex'); - const sessionId = crypto.createHash('md5').update(`3${Date.now()}`).digest('hex'); + const ticketId = functions.MakeID().replace(/-/ig, ""); + const matchId = functions.MakeID().replace(/-/ig, ""); + const sessionId = functions.MakeID().replace(/-/ig, ""); - // you can use setTimeout to send the websocket messages at certain times - setTimeout(Connecting, 200/* Milliseconds */); - setTimeout(Waiting, 1000); // 0.8 Seconds after Connecting - setTimeout(Queued, 2000); // 1 Second after Waiting - setTimeout(SessionAssignment, 6000); // 4 Seconds after Queued - setTimeout(Join, 8000); // 2 Seconds after SessionAssignment + Connecting(); + await functions.sleep(800); + Waiting(); + await functions.sleep(1000); + Queued(); + await functions.sleep(4000); + SessionAssignment(); + await functions.sleep(2000); + Join(); function Connecting() { ws.send(JSON.stringify({ diff --git a/routes/auth.js b/routes/auth.js index e442e82..9a42837 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -20,10 +20,10 @@ app.post("/account/api/oauth/token", async (req, res) => { clientId = clientId[0]; } catch { - return res.status(400).json(error.createError( + return error.createError( "errors.com.epicgames.common.oauth.invalid_client", "It appears that your Authorization header may be invalid or not present, please verify that you are sending the correct headers.", - [], 1011, "invalid_client") + [], 1011, "invalid_client", 400, res ); } @@ -50,32 +50,32 @@ app.post("/account/api/oauth/token", async (req, res) => { break; case "password": - if (!req.body.username || !req.body.password) return res.status(400).json(error.createError( + if (!req.body.username || !req.body.password) return error.createError( "errors.com.epicgames.common.oauth.invalid_request", "Username/password is required.", - [], 1013, "invalid_request") + [], 1013, "invalid_request", 400, res ); const { username: email, password: password } = req.body; req.user = await User.findOne({ email: email.toLowerCase() }).lean(); - let err = error.createError( + let err = () => error.createError( "errors.com.epicgames.account.invalid_account_credentials", "Your e-mail and/or password are incorrect. Please check them and try again.", - [], 18031, "invalid_grant" + [], 18031, "invalid_grant", 400, res ); - if (!req.user) return res.status(400).json(err); + if (!req.user) return err(); else { - if (!await bcrypt.compare(password, req.user.password)) return res.status(400).json(err); + if (!await bcrypt.compare(password, req.user.password)) return err(); } break; case "refresh_token": - if (!req.body.refresh_token) return res.status(400).json(error.createError( + if (!req.body.refresh_token) return error.createError( "errors.com.epicgames.common.oauth.invalid_request", "Refresh token is required.", - [], 1013, "invalid_request") + [], 1013, "invalid_request", 400, res ); const refresh_token = req.body.refresh_token; @@ -84,16 +84,16 @@ app.post("/account/api/oauth/token", async (req, res) => { let object = global.refreshTokens[refreshToken]; try { - jwt.verify(refresh_token.replace("eg1~", ""), global.JWT_SECRET); - if (refreshToken == -1) throw new Error("Refresh token invalid."); + + jwt.verify(refresh_token.replace("eg1~", ""), global.JWT_SECRET); } catch { if (refreshToken != -1) global.refreshTokens.splice(refreshToken, 1); - res.status(400).json(error.createError( + error.createError( "errors.com.epicgames.account.auth_token.invalid_refresh_token", `Sorry the refresh token '${refresh_token}' is invalid`, - [refresh_token], 18036, "invalid_grant") + [refresh_token], 18036, "invalid_grant", 400, res ); return; @@ -103,10 +103,10 @@ app.post("/account/api/oauth/token", async (req, res) => { break; case "exchange_code": - if (!req.body.exchange_code) return res.status(400).json(error.createError( + if (!req.body.exchange_code) return error.createError( "errors.com.epicgames.common.oauth.invalid_request", "Exchange code is required.", - [], 1013, "invalid_request") + [], 1013, "invalid_request", 400, res ); const { exchange_code } = req.body; @@ -114,10 +114,10 @@ app.post("/account/api/oauth/token", async (req, res) => { let index = global.exchangeCodes.findIndex(i => i.exchange_code == exchange_code); let exchange = global.exchangeCodes[index]; - if (index == -1) return res.status(400).json(error.createError( + if (index == -1) return error.createError( "errors.com.epicgames.account.oauth.exchange_code_not_found", "Sorry the exchange code you supplied was not found. It is possible that it was no longer valid", - [], 18057, "invalid_grant") + [], 18057, "invalid_grant", 400, res ); global.exchangeCodes.splice(index, 1); @@ -126,18 +126,18 @@ app.post("/account/api/oauth/token", async (req, res) => { break; default: - res.status(400).json(error.createError( + error.createError( "errors.com.epicgames.common.oauth.unsupported_grant_type", `Unsupported grant type: ${req.body.grant_type}`, - [], 1016, "unsupported_grant_type") + [], 1016, "unsupported_grant_type", 400, res ); return; } - if (req.user.banned) return res.status(400).json(error.createError( + if (req.user.banned) return error.createError( "errors.com.epicgames.account.account_not_active", "Sorry, your account is inactive and may not login.", - [], -1, undefined) + [], -1, undefined, 400, res ); let accessIndex = global.accessTokens.findIndex(i => i.accountId == req.user.accountId); diff --git a/routes/mcp.js b/routes/mcp.js index 71f8ec3..c03f877 100644 --- a/routes/mcp.js +++ b/routes/mcp.js @@ -9,13 +9,14 @@ const functions = require("../structs/functions.js"); const { verifyToken, verifyClient } = require("../tokenManager/tokenVerify.js"); app.post("/fortnite/api/game/v2/profile/*/client/MarkItemSeen", verifyToken, async (req, res) => { - if (!await profileManager.validateProfile(req.user.accountId, req.query.profileId)) return res.status(403).json(error.createError( + const profiles = await Profile.findOne({ accountId: req.user.accountId }); + + if (!await profileManager.validateProfile(req.query.profileId, profiles.lean())) return error.createError( "errors.com.epicgames.modules.profiles.operation_forbidden", `Unable to find template configuration for profile ${req.query.profileId}`, - [req.query.profileId], 12813, undefined) + [req.query.profileId], 12813, undefined, 403, res ); - const profiles = await Profile.findOne({ accountId: req.user.accountId }); let profile = profiles.profiles[req.query.profileId]; if (req.query.profileId == "athena") { @@ -31,13 +32,13 @@ app.post("/fortnite/api/game/v2/profile/*/client/MarkItemSeen", verifyToken, asy let missingFields = checkFields(["itemIds"], req.body); - if (missingFields.fields.length > 0) return res.status(400).json(error.createError( + if (missingFields.fields.length > 0) return error.createError( "errors.com.epicgames.validation.validation_failed", `Validation Failed. [${missingFields.fields.join(", ")}] field(s) is missing.`, - [`[${missingFields.fields.join(", ")}]`], 1040, undefined) + [`[${missingFields.fields.join(", ")}]`], 1040, undefined, 400, res ); - if (!Array.isArray(req.body.itemIds)) return res.status(400).json(ValidationError("itemIds", "an array")); + if (!Array.isArray(req.body.itemIds)) return ValidationError("itemIds", "an array", res); if (!profile.items) profile.items = {}; @@ -83,19 +84,20 @@ app.post("/fortnite/api/game/v2/profile/*/client/MarkItemSeen", verifyToken, asy }); app.post("/fortnite/api/game/v2/profile/*/client/SetItemFavoriteStatusBatch", verifyToken, async (req, res) => { - if (!await profileManager.validateProfile(req.user.accountId, req.query.profileId)) return res.status(403).json(error.createError( + const profiles = await Profile.findOne({ accountId: req.user.accountId }); + + if (!await profileManager.validateProfile(req.query.profileId, profiles.lean())) return error.createError( "errors.com.epicgames.modules.profiles.operation_forbidden", `Unable to find template configuration for profile ${req.query.profileId}`, - [req.query.profileId], 12813, undefined) + [req.query.profileId], 12813, undefined, 403, res ); - if (req.query.profileId != "athena") return res.status(400).json(error.createError( + if (req.query.profileId != "athena") return error.createError( "errors.com.epicgames.modules.profiles.invalid_command", `SetItemFavoriteStatusBatch is not valid on ${req.query.profileId} profile`, - ["SetItemFavoriteStatusBatch",req.query.profileId], 12801, undefined) + ["SetItemFavoriteStatusBatch",req.query.profileId], 12801, undefined, 400, res ); - const profiles = await Profile.findOne({ accountId: req.user.accountId }); let profile = profiles.profiles[req.query.profileId]; if (req.query.profileId == "athena") { @@ -111,14 +113,14 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetItemFavoriteStatusBatch", ve let missingFields = checkFields(["itemIds","itemFavStatus"], req.body); - if (missingFields.fields.length > 0) return res.status(400).json(error.createError( + if (missingFields.fields.length > 0) return error.createError( "errors.com.epicgames.validation.validation_failed", `Validation Failed. [${missingFields.fields.join(", ")}] field(s) is missing.`, - [`[${missingFields.fields.join(", ")}]`], 1040, undefined) + [`[${missingFields.fields.join(", ")}]`], 1040, undefined, 400, res ); - if (!Array.isArray(req.body.itemIds)) return res.status(400).json(ValidationError("itemIds", "an array")); - if (!Array.isArray(req.body.itemFavStatus)) return res.status(400).json(ValidationError("itemFavStatus", "an array")); + if (!Array.isArray(req.body.itemIds)) return ValidationError("itemIds", "an array", res); + if (!Array.isArray(req.body.itemFavStatus)) return ValidationError("itemFavStatus", "an array", res); if (!profile.items) profile.items = {}; @@ -165,19 +167,20 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetItemFavoriteStatusBatch", ve }); app.post("/fortnite/api/game/v2/profile/*/client/SetBattleRoyaleBanner", verifyToken, async (req, res) => { - if (!await profileManager.validateProfile(req.user.accountId, req.query.profileId)) return res.status(403).json(error.createError( + const profiles = await Profile.findOne({ accountId: req.user.accountId }); + + if (!await profileManager.validateProfile(req.query.profileId, profiles.lean())) return error.createError( "errors.com.epicgames.modules.profiles.operation_forbidden", `Unable to find template configuration for profile ${req.query.profileId}`, - [req.query.profileId], 12813, undefined) + [req.query.profileId], 12813, undefined, 403, res ); - if (req.query.profileId != "athena") return res.status(400).json(error.createError( + if (req.query.profileId != "athena") return error.createError( "errors.com.epicgames.modules.profiles.invalid_command", `SetBattleRoyaleBanner is not valid on ${req.query.profileId} profile`, - ["SetBattleRoyaleBanner",req.query.profileId], 12801, undefined) + ["SetBattleRoyaleBanner",req.query.profileId], 12801, undefined, 400, res ); - const profiles = await Profile.findOne({ accountId: req.user.accountId }); let profile = profiles.profiles[req.query.profileId]; const memory = functions.GetVersionInfo(req); @@ -191,14 +194,14 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetBattleRoyaleBanner", verifyT let missingFields = checkFields(["homebaseBannerIconId","homebaseBannerColorId"], req.body); - if (missingFields.fields.length > 0) return res.status(400).json(error.createError( + if (missingFields.fields.length > 0) return error.createError( "errors.com.epicgames.validation.validation_failed", `Validation Failed. [${missingFields.fields.join(", ")}] field(s) is missing.`, - [`[${missingFields.fields.join(", ")}]`], 1040, undefined) + [`[${missingFields.fields.join(", ")}]`], 1040, undefined, 400, res ); - if (typeof req.body.homebaseBannerIconId != "string") return res.status(400).json(ValidationError("homebaseBannerIconId", "a string")); - if (typeof req.body.homebaseBannerColorId != "string") return res.status(400).json(ValidationError("homebaseBannerColorId", "a string")); + if (typeof req.body.homebaseBannerIconId != "string") return ValidationError("homebaseBannerIconId", "a string", res); + if (typeof req.body.homebaseBannerColorId != "string") return ValidationError("homebaseBannerColorId", "a string", res); let bannerProfileId = memory.build < 3.5 ? "profile0" : "common_core"; @@ -214,16 +217,16 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetBattleRoyaleBanner", verifyT if (HomebaseBannerIconID && HomebaseBannerColorID) break; } - if (!HomebaseBannerIconID) return res.status(400).json(error.createError( + if (!HomebaseBannerIconID) return error.createError( "errors.com.epicgames.fortnite.item_not_found", `Banner template 'HomebaseBannerIcon:${req.body.homebaseBannerIconId}' not found in profile`, - [`HomebaseBannerIcon:${req.body.homebaseBannerIconId}`], 16006, undefined) + [`HomebaseBannerIcon:${req.body.homebaseBannerIconId}`], 16006, undefined, 400, res ); - if (!HomebaseBannerColorID) return res.status(400).json(error.createError( + if (!HomebaseBannerColorID) return error.createError( "errors.com.epicgames.fortnite.item_not_found", `Banner template 'HomebaseBannerColor:${req.body.homebaseBannerColorId}' not found in profile`, - [`HomebaseBannerColor:${req.body.homebaseBannerColorId}`], 16006, undefined) + [`HomebaseBannerColor:${req.body.homebaseBannerColorId}`], 16006, undefined, 400, res ); if (!profile.items) profile.items = {}; @@ -277,19 +280,20 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetBattleRoyaleBanner", verifyT }); app.post("/fortnite/api/game/v2/profile/*/client/EquipBattleRoyaleCustomization", verifyToken, async (req, res) => { - if (!await profileManager.validateProfile(req.user.accountId, req.query.profileId)) return res.status(403).json(error.createError( + const profiles = await Profile.findOne({ accountId: req.user.accountId }); + + if (!await profileManager.validateProfile(req.query.profileId, profiles.lean())) return error.createError( "errors.com.epicgames.modules.profiles.operation_forbidden", `Unable to find template configuration for profile ${req.query.profileId}`, - [req.query.profileId], 12813, undefined) + [req.query.profileId], 12813, undefined, 403, res ); - if (req.query.profileId != "athena") return res.status(400).json(error.createError( + if (req.query.profileId != "athena") return error.createError( "errors.com.epicgames.modules.profiles.invalid_command", `EquipBattleRoyaleCustomization is not valid on ${req.query.profileId} profile`, - ["EquipBattleRoyaleCustomization",req.query.profileId], 12801, undefined) + ["EquipBattleRoyaleCustomization",req.query.profileId], 12801, undefined, 400, res ); - const profiles = await Profile.findOne({ accountId: req.user.accountId }); let profile = profiles.profiles[req.query.profileId]; if (req.query.profileId == "athena") { @@ -315,14 +319,14 @@ app.post("/fortnite/api/game/v2/profile/*/client/EquipBattleRoyaleCustomization" let missingFields = checkFields(["slotName"], req.body); - if (missingFields.fields.length > 0) return res.status(400).json(error.createError( + if (missingFields.fields.length > 0) return error.createError( "errors.com.epicgames.validation.validation_failed", `Validation Failed. [${missingFields.fields.join(", ")}] field(s) is missing.`, - [`[${missingFields.fields.join(", ")}]`], 1040, undefined) + [`[${missingFields.fields.join(", ")}]`], 1040, undefined, 400, res ); - if (typeof req.body.itemToSlot != "string") return res.status(400).json(ValidationError("itemToSlot", "a string")); - if (typeof req.body.slotName != "string") return res.status(400).json(ValidationError("slotName", "a string")); + if (typeof req.body.itemToSlot != "string") return ValidationError("itemToSlot", "a string", res); + if (typeof req.body.slotName != "string") return ValidationError("slotName", "a string", res); if (!profile.items) profile.items = {}; @@ -330,25 +334,25 @@ app.post("/fortnite/api/game/v2/profile/*/client/EquipBattleRoyaleCustomization" let item = req.body.itemToSlot.toLowerCase(); if (!specialCosmetics.includes(item)) { - return res.status(400).json(error.createError( + return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Item (id: "${req.body.itemToSlot}") not found`, - [req.body.itemToSlot], 16027, undefined) + [req.body.itemToSlot], 16027, undefined, 400, res ); } else { - if (!item.startsWith((`Athena${req.body.slotName}:`).toLowerCase())) return res.status(400).json(error.createError( + if (!item.startsWith((`Athena${req.body.slotName}:`).toLowerCase())) return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Cannot slot item of type ${item.split(":")[0]} in slot of category ${req.body.slotName}`, - [item.split(":")[0],req.body.slotName], 16027, undefined) + [item.split(":")[0],req.body.slotName], 16027, undefined, 400, res ); } } if (profile.items[req.body.itemToSlot]) { - if (!profile.items[req.body.itemToSlot].templateId.startsWith(`Athena${req.body.slotName}:`)) return res.status(400).json(error.createError( + if (!profile.items[req.body.itemToSlot].templateId.startsWith(`Athena${req.body.slotName}:`)) return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Cannot slot item of type ${profile.items[req.body.itemToSlot].templateId.split(":")[0]} in slot of category ${req.body.slotName}`, - [profile.items[req.body.itemToSlot].templateId.split(":")[0],req.body.slotName], 16027, undefined) + [profile.items[req.body.itemToSlot].templateId.split(":")[0],req.body.slotName], 16027, undefined, 400, res ); let Variants = req.body.variantUpdates; @@ -470,19 +474,20 @@ app.post("/fortnite/api/game/v2/profile/*/client/EquipBattleRoyaleCustomization" }); app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerBanner", verifyToken, async (req, res) => { - if (!await profileManager.validateProfile(req.user.accountId, req.query.profileId)) return res.status(403).json(error.createError( + const profiles = await Profile.findOne({ accountId: req.user.accountId }); + + if (!await profileManager.validateProfile(req.query.profileId, profiles.lean())) return error.createError( "errors.com.epicgames.modules.profiles.operation_forbidden", `Unable to find template configuration for profile ${req.query.profileId}`, - [req.query.profileId], 12813, undefined) + [req.query.profileId], 12813, undefined, 403, res ); - if (req.query.profileId != "athena") return res.status(400).json(error.createError( + if (req.query.profileId != "athena") return error.createError( "errors.com.epicgames.modules.profiles.invalid_command", `SetCosmeticLockerBanner is not valid on ${req.query.profileId} profile`, - ["SetCosmeticLockerBanner",req.query.profileId], 12801, undefined) + ["SetCosmeticLockerBanner",req.query.profileId], 12801, undefined, 400, res ); - const profiles = await Profile.findOne({ accountId: req.user.accountId }); let profile = profiles.profiles[req.query.profileId]; if (req.query.profileId == "athena") { @@ -498,28 +503,28 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerBanner", verif let missingFields = checkFields(["bannerIconTemplateName","bannerColorTemplateName","lockerItem"], req.body); - if (missingFields.fields.length > 0) return res.status(400).json(error.createError( + if (missingFields.fields.length > 0) return error.createError( "errors.com.epicgames.validation.validation_failed", `Validation Failed. [${missingFields.fields.join(", ")}] field(s) is missing.`, - [`[${missingFields.fields.join(", ")}]`], 1040, undefined) + [`[${missingFields.fields.join(", ")}]`], 1040, undefined, 400, res ); - if (typeof req.body.lockerItem != "string") return res.status(400).json(ValidationError("lockerItem", "a string")); - if (typeof req.body.bannerIconTemplateName != "string") return res.status(400).json(ValidationError("bannerIconTemplateName", "a string")); - if (typeof req.body.bannerColorTemplateName != "string") return res.status(400).json(ValidationError("bannerColorTemplateName", "a string")); + if (typeof req.body.lockerItem != "string") return ValidationError("lockerItem", "a string", res); + if (typeof req.body.bannerIconTemplateName != "string") return ValidationError("bannerIconTemplateName", "a string", res); + if (typeof req.body.bannerColorTemplateName != "string") return ValidationError("bannerColorTemplateName", "a string", res); if (!profile.items) profile.items = {}; - if (!profile.items[req.body.lockerItem]) return res.status(400).json(error.createError( + if (!profile.items[req.body.lockerItem]) return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Item (id: "${req.body.lockerItem}") not found`, - [req.body.lockerItem], 16027, undefined) + [req.body.lockerItem], 16027, undefined, 400, res ); - if (profile.items[req.body.lockerItem].templateId.toLowerCase() != "cosmeticlocker:cosmeticlocker_athena") return res.status(400).json(error.createError( + if (profile.items[req.body.lockerItem].templateId.toLowerCase() != "cosmeticlocker:cosmeticlocker_athena") return error.createError( "errors.com.epicgames.fortnite.id_invalid", `lockerItem id is not a cosmeticlocker`, - ["lockerItem"], 16027, undefined) + ["lockerItem"], 16027, undefined, 400, res ); let bannerProfileId = "common_core"; @@ -536,16 +541,16 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerBanner", verif if (HomebaseBannerIconID && HomebaseBannerColorID) break; } - if (!HomebaseBannerIconID) return res.status(400).json(error.createError( + if (!HomebaseBannerIconID) return error.createError( "errors.com.epicgames.fortnite.item_not_found", `Banner template 'HomebaseBannerIcon:${req.body.bannerIconTemplateName}' not found in profile`, - [`HomebaseBannerIcon:${req.body.bannerIconTemplateName}`], 16006, undefined) + [`HomebaseBannerIcon:${req.body.bannerIconTemplateName}`], 16006, undefined, 400, res ); - if (!HomebaseBannerColorID) return res.status(400).json(error.createError( + if (!HomebaseBannerColorID) return error.createError( "errors.com.epicgames.fortnite.item_not_found", `Banner template 'HomebaseBannerColor:${req.body.bannerColorTemplateName}' not found in profile`, - [`HomebaseBannerColor:${req.body.bannerColorTemplateName}`], 16006, undefined) + [`HomebaseBannerColor:${req.body.bannerColorTemplateName}`], 16006, undefined, 400, res ); profile.items[req.body.lockerItem].attributes.banner_icon_template = req.body.bannerIconTemplateName; @@ -597,19 +602,20 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerBanner", verif }); app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerSlot", verifyToken, async (req, res) => { - if (!await profileManager.validateProfile(req.user.accountId, req.query.profileId)) return res.status(403).json(error.createError( + const profiles = await Profile.findOne({ accountId: req.user.accountId }); + + if (!await profileManager.validateProfile(req.query.profileId, profiles.lean())) return error.createError( "errors.com.epicgames.modules.profiles.operation_forbidden", `Unable to find template configuration for profile ${req.query.profileId}`, - [req.query.profileId], 12813, undefined) + [req.query.profileId], 12813, undefined, 403, res ); - if (req.query.profileId != "athena") return res.status(400).json(error.createError( + if (req.query.profileId != "athena") return error.createError( "errors.com.epicgames.modules.profiles.invalid_command", `SetCosmeticLockerSlot is not valid on ${req.query.profileId} profile`, - ["SetCosmeticLockerSlot",req.query.profileId], 12801, undefined) + ["SetCosmeticLockerSlot",req.query.profileId], 12801, undefined, 400, res ); - const profiles = await Profile.findOne({ accountId: req.user.accountId }); let profile = profiles.profiles[req.query.profileId]; if (req.query.profileId == "athena") { @@ -635,15 +641,15 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerSlot", verifyT let missingFields = checkFields(["category","lockerItem"], req.body); - if (missingFields.fields.length > 0) return res.status(400).json(error.createError( + if (missingFields.fields.length > 0) return error.createError( "errors.com.epicgames.validation.validation_failed", `Validation Failed. [${missingFields.fields.join(", ")}] field(s) is missing.`, - [`[${missingFields.fields.join(", ")}]`], 1040, undefined) + [`[${missingFields.fields.join(", ")}]`], 1040, undefined, 400, res ); - if (typeof req.body.itemToSlot != "string") return res.status(400).json(ValidationError("itemToSlot", "a string")); - if (typeof req.body.lockerItem != "string") return res.status(400).json(ValidationError("lockerItem", "a string")); - if (typeof req.body.category != "string") return res.status(400).json(ValidationError("category", "a string")); + if (typeof req.body.itemToSlot != "string") return ValidationError("itemToSlot", "a string", res); + if (typeof req.body.lockerItem != "string") return ValidationError("lockerItem", "a string", res); + if (typeof req.body.category != "string") return ValidationError("category", "a string", res); if (!profile.items) profile.items = {}; @@ -655,41 +661,41 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerSlot", verifyT } } - if (!profile.items[req.body.lockerItem]) return res.status(400).json(error.createError( + if (!profile.items[req.body.lockerItem]) return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Item (id: "${req.body.lockerItem}") not found`, - [req.body.lockerItem], 16027, undefined) + [req.body.lockerItem], 16027, undefined, 400, res ); - if (profile.items[req.body.lockerItem].templateId.toLowerCase() != "cosmeticlocker:cosmeticlocker_athena") return res.status(400).json(error.createError( + if (profile.items[req.body.lockerItem].templateId.toLowerCase() != "cosmeticlocker:cosmeticlocker_athena") return error.createError( "errors.com.epicgames.fortnite.id_invalid", `lockerItem id is not a cosmeticlocker`, - ["lockerItem"], 16027, undefined) + ["lockerItem"], 16027, undefined, 400, res ); if (!profile.items[itemToSlotID] && req.body.itemToSlot) { let item = req.body.itemToSlot.toLowerCase(); if (!specialCosmetics.includes(item)) { - return res.status(400).json(error.createError( + return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Item (id: "${req.body.itemToSlot}") not found`, - [req.body.itemToSlot], 16027, undefined) + [req.body.itemToSlot], 16027, undefined, 400, res ); } else { - if (!item.startsWith((`Athena${req.body.category}:`).toLowerCase())) return res.status(400).json(error.createError( + if (!item.startsWith((`Athena${req.body.category}:`).toLowerCase())) return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Cannot slot item of type ${item.split(":")[0]} in slot of category ${req.body.category}`, - [item.split(":")[0],req.body.category], 16027, undefined) + [item.split(":")[0],req.body.category], 16027, undefined, 400, res ); } } if (profile.items[itemToSlotID]) { - if (!profile.items[itemToSlotID].templateId.startsWith(`Athena${req.body.category}:`)) return res.status(400).json(error.createError( + if (!profile.items[itemToSlotID].templateId.startsWith(`Athena${req.body.category}:`)) return error.createError( "errors.com.epicgames.fortnite.id_invalid", `Cannot slot item of type ${profile.items[itemToSlotID].templateId.split(":")[0]} in slot of category ${req.body.category}`, - [profile.items[itemToSlotID].templateId.split(":")[0],req.body.category], 16027, undefined) + [profile.items[itemToSlotID].templateId.split(":")[0],req.body.category], 16027, undefined, 400, res ); let Variants = req.body.variantUpdates; @@ -803,13 +809,14 @@ app.post("/fortnite/api/game/v2/profile/*/client/SetCosmeticLockerSlot", verifyT }); app.post("/fortnite/api/game/v2/profile/*/client/:operation", verifyToken, async (req, res) => { - if (!await profileManager.validateProfile(req.user.accountId, req.query.profileId)) return res.status(403).json(error.createError( + const profiles = await Profile.findOne({ accountId: req.user.accountId }); + + if (!await profileManager.validateProfile(req.query.profileId, profiles.lean())) return error.createError( "errors.com.epicgames.modules.profiles.operation_forbidden", `Unable to find template configuration for profile ${req.query.profileId}`, - [req.query.profileId], 12813, undefined) + [req.query.profileId], 12813, undefined, 403, res ); - - const profiles = await Profile.findOne({ accountId: req.user.accountId }).lean(); + let profile = profiles.profiles[req.query.profileId]; if (req.query.profileId == "athena") { @@ -833,10 +840,10 @@ app.post("/fortnite/api/game/v2/profile/*/client/:operation", verifyToken, async case "PurchaseCatalogEntry": break; default: - res.status(404).json(error.createError( + error.createError( "errors.com.epicgames.fortnite.operation_not_found", `Operation ${req.params.operation} not valid`, - [req.params.operation], 16035, undefined) + [req.params.operation], 16035, undefined, 404, res ); return; } @@ -869,11 +876,11 @@ function checkFields(fields, body) { return missingFields; } -function ValidationError(field, type) { +function ValidationError(field, type, res) { return error.createError( "errors.com.epicgames.validation.validation_failed", `Validation Failed. '${field}' is not ${type}.`, - [field], 1040, undefined + [field], 1040, undefined, 400, res ); } diff --git a/routes/user.js b/routes/user.js index 556534a..4998d8e 100644 --- a/routes/user.js +++ b/routes/user.js @@ -42,10 +42,10 @@ app.get("/account/api/public/account", async (req, res) => { app.get("/account/api/public/account/displayName/:displayName", async (req, res) => { let user = await User.findOne({ username_lower: req.params.displayName.toLowerCase(), banned: false }).lean(); - if (!user) return res.status(404).json(error.createError( + if (!user) return error.createError( "errors.com.epicgames.account.account_not_found", `Sorry, we couldn't find an account for ${req.params.displayName}`, - [req.params.displayName], 18007, undefined) + [req.params.displayName], 18007, undefined, 404, res ); res.json({ @@ -56,17 +56,17 @@ app.get("/account/api/public/account/displayName/:displayName", async (req, res) }); app.get("/persona/api/public/account/lookup", async (req, res) => { - if (typeof req.query.q != "string") return res.status(400).json(error.createError( + if (typeof req.query.q != "string") return error.createError( "errors.com.epicgames.bad_request", "Required String parameter 'q' is invalid or not present", - undefined, 1001, undefined) + undefined, 1001, undefined, 400, res ); let user = await User.findOne({ username_lower: req.query.q.toLowerCase(), banned: false }).lean(); - if (!user) return res.status(404).json(error.createError( + if (!user) return error.createError( "errors.com.epicgames.account.account_not_found", `Sorry, we couldn't find an account for ${req.query.q}`, - [req.query.q], 18007, undefined) + [req.query.q], 18007, undefined, 404, res ); res.json({ @@ -79,10 +79,10 @@ app.get("/persona/api/public/account/lookup", async (req, res) => { app.get("/api/v1/search/:accountId", async (req, res) => { let response = []; - if (typeof req.query.prefix != "string") return res.status(400).json(error.createError( + if (typeof req.query.prefix != "string") return error.createError( "errors.com.epicgames.bad_request", "Required String parameter 'prefix' is invalid or not present", - undefined, 1001, undefined) + undefined, 1001, undefined, 400, res ); let users = await User.find({ username_lower: new RegExp(`^${req.query.prefix.toLowerCase()}`), banned: false }).lean(); diff --git a/structs/error.js b/structs/error.js index 61276a7..122a219 100644 --- a/structs/error.js +++ b/structs/error.js @@ -1,5 +1,10 @@ -function createError(errorCode, errorMessage, messageVars, numericErrorCode, error) { - return { +function createError(errorCode, errorMessage, messageVars, numericErrorCode, error, statusCode, res) { + res.set({ + 'X-Epic-Error-Name': errorCode, + 'X-Epic-Error-Code': numericErrorCode + }); + + res.status(statusCode).json({ errorCode: errorCode, errorMessage: errorMessage, messageVars: messageVars, @@ -8,7 +13,7 @@ function createError(errorCode, errorMessage, messageVars, numericErrorCode, err intent: "prod", error_description: errorMessage, error: error - }; + }); } module.exports = { diff --git a/structs/profile.js b/structs/profile.js index 22dfeb5..900a060 100644 --- a/structs/profile.js +++ b/structs/profile.js @@ -17,9 +17,8 @@ function createProfiles(accountId) { return profiles; } -async function validateProfile(accountId, profileId) { +async function validateProfile(profileId, profiles) { try { - let profiles = await Profile.findOne({ accountId: accountId }).lean(); let profile = profiles.profiles[profileId]; if (!profile || !profileId) throw new Error("Invalid profile/profileId"); diff --git a/tokenManager/tokenVerify.js b/tokenManager/tokenVerify.js index acf64da..1db7294 100644 --- a/tokenManager/tokenVerify.js +++ b/tokenManager/tokenVerify.js @@ -4,13 +4,13 @@ const User = require("../model/user.js"); const error = require("../structs/error.js"); async function verifyToken(req, res, next) { - let authErr = error.createError( + let authErr = () => error.createError( "errors.com.epicgames.common.authorization.authorization_failed", `Authorization failed for ${req.originalUrl}`, - [req.originalUrl], 1032, undefined + [req.originalUrl], 1032, undefined, 401, res ); - if (!req.headers["authorization"] || !req.headers["authorization"].startsWith("bearer eg1~")) return res.status(401).json(authErr); + if (!req.headers["authorization"] || !req.headers["authorization"].startsWith("bearer eg1~")) return authErr(); const token = req.headers["authorization"].replace("bearer eg1~", ""); @@ -21,10 +21,10 @@ async function verifyToken(req, res, next) { req.user = await User.findOne({ accountId: decodedToken.sub }).lean(); - if (req.user.banned) return res.status(400).json(error.createError( + if (req.user.banned) return error.createError( "errors.com.epicgames.account.account_not_active", "Sorry, your account is inactive and may not login.", - [], -1, undefined) + [], -1, undefined, 400, res ); next(); @@ -32,18 +32,18 @@ async function verifyToken(req, res, next) { let accessIndex = global.accessTokens.findIndex(i => i.token == `eg1~${token}`); if (accessIndex != -1) global.accessTokens.splice(accessIndex, 1); - return res.status(401).json(authErr); + return authErr(); } } async function verifyClient(req, res, next) { - let authErr = error.createError( + let authErr = () => error.createError( "errors.com.epicgames.common.authorization.authorization_failed", `Authorization failed for ${req.originalUrl}`, - [req.originalUrl], 1032, undefined + [req.originalUrl], 1032, undefined, 401, res ); - if (!req.headers["authorization"] || !req.headers["authorization"].startsWith("bearer eg1~")) return res.status(401).json(authErr); + if (!req.headers["authorization"] || !req.headers["authorization"].startsWith("bearer eg1~")) return authErr(); const token = req.headers["authorization"].replace("bearer eg1~", ""); @@ -57,10 +57,10 @@ async function verifyClient(req, res, next) { if (findAccess) { req.user = await User.findOne({ accountId: decodedToken.sub }).lean(); - if (req.user.banned) return res.status(400).json(error.createError( + if (req.user.banned) return error.createError( "errors.com.epicgames.account.account_not_active", "Sorry, your account is inactive and may not login.", - [], -1, undefined) + [], -1, undefined, 400, res ); } @@ -72,7 +72,7 @@ async function verifyClient(req, res, next) { let clientIndex = global.clientTokens.findIndex(i => i.token == `eg1~${token}`); if (clientIndex != -1) global.clientTokens.splice(clientIndex, 1); - return res.status(401).json(authErr); + return authErr(); } }