From 4b2bb35c20d40a8c65027d628c5990b8d470b4f8 Mon Sep 17 00:00:00 2001 From: yaoyanwei Date: Mon, 4 Aug 2025 16:25:38 +0800 Subject: [PATCH] init --- .gitignore | 68 ++++ activity/activity.controller.js | 21 ++ activity/activity.model.js | 56 ++++ activity/activity.routes.js | 19 ++ authorization/auth.controller.js | 94 ++++++ authorization/auth.routes.js | 44 +++ authorization/auth.tool.js | 162 +++++++++ cards/cards.controller.js | 134 ++++++++ cards/cards.model.js | 111 +++++++ cards/cards.routes.js | 42 +++ cards/cards.tool.js | 111 +++++++ config.js | 73 +++++ decks/decks.controller.js | 75 +++++ decks/decks.model.js | 99 ++++++ decks/decks.routes.js | 36 ++ jobs/jobs.js | 21 ++ market/market.controller.js | 215 ++++++++++++ market/market.model.js | 146 +++++++++ market/market.routes.js | 51 +++ matches/matches.controller.js | 123 +++++++ matches/matches.model.js | 87 +++++ matches/matches.routes.js | 34 ++ matches/matches.tool.js | 71 ++++ package.json | 16 + packs/packs.controller.js | 80 +++++ packs/packs.model.js | 96 ++++++ packs/packs.routes.js | 36 ++ rewards/rewards.controller.js | 99 ++++++ rewards/rewards.model.js | 107 ++++++ rewards/rewards.routes.js | 40 +++ server.js | 134 ++++++++ tools/date.tool.js | 65 ++++ tools/email.tool.js | 121 +++++++ tools/file.tool.js | 16 + tools/limiter.tool.js | 46 +++ tools/validator.tool.js | 92 ++++++ tools/web.tool.js | 100 ++++++ users/users.cards.controller.js | 529 ++++++++++++++++++++++++++++++ users/users.controller.js | 505 ++++++++++++++++++++++++++++ users/users.friends.controller.js | 135 ++++++++ users/users.model.js | 246 ++++++++++++++ users/users.routes.js | 215 ++++++++++++ users/users.tool.js | 352 ++++++++++++++++++++ variants/variants.controller.js | 66 ++++ variants/variants.model.js | 103 ++++++ variants/variants.routes.js | 36 ++ 46 files changed, 5128 insertions(+) create mode 100644 .gitignore create mode 100644 activity/activity.controller.js create mode 100644 activity/activity.model.js create mode 100644 activity/activity.routes.js create mode 100644 authorization/auth.controller.js create mode 100644 authorization/auth.routes.js create mode 100644 authorization/auth.tool.js create mode 100644 cards/cards.controller.js create mode 100644 cards/cards.model.js create mode 100644 cards/cards.routes.js create mode 100644 cards/cards.tool.js create mode 100644 config.js create mode 100644 decks/decks.controller.js create mode 100644 decks/decks.model.js create mode 100644 decks/decks.routes.js create mode 100644 jobs/jobs.js create mode 100644 market/market.controller.js create mode 100644 market/market.model.js create mode 100644 market/market.routes.js create mode 100644 matches/matches.controller.js create mode 100644 matches/matches.model.js create mode 100644 matches/matches.routes.js create mode 100644 matches/matches.tool.js create mode 100644 package.json create mode 100644 packs/packs.controller.js create mode 100644 packs/packs.model.js create mode 100644 packs/packs.routes.js create mode 100644 rewards/rewards.controller.js create mode 100644 rewards/rewards.model.js create mode 100644 rewards/rewards.routes.js create mode 100644 server.js create mode 100644 tools/date.tool.js create mode 100644 tools/email.tool.js create mode 100644 tools/file.tool.js create mode 100644 tools/limiter.tool.js create mode 100644 tools/validator.tool.js create mode 100644 tools/web.tool.js create mode 100644 users/users.cards.controller.js create mode 100644 users/users.controller.js create mode 100644 users/users.friends.controller.js create mode 100644 users/users.model.js create mode 100644 users/users.routes.js create mode 100644 users/users.tool.js create mode 100644 variants/variants.controller.js create mode 100644 variants/variants.model.js create mode 100644 variants/variants.routes.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca724cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +#---- +#Unity +unity/ +uploads/ +public/.well-known + diff --git a/activity/activity.controller.js b/activity/activity.controller.js new file mode 100644 index 0000000..548c398 --- /dev/null +++ b/activity/activity.controller.js @@ -0,0 +1,21 @@ +// MODELS / TOOLS +const Activity = require("./activity.model"); + +exports.GetAllActivities = async (req, res) => { + + let activityRequest; + if (req.body.type) { + activityRequest = { type: req.body.type }; + } else if (req.body.username) { + activityRequest = { username: req.body.username }; + } + else { + activityRequest = { }; + } + + const a = await Activity.Get(activityRequest); + if (!a) return res.status(500).send({ error: "Failed!!" }); + + return res.status(200).send(a); +}; + diff --git a/activity/activity.model.js b/activity/activity.model.js new file mode 100644 index 0000000..841fbd8 --- /dev/null +++ b/activity/activity.model.js @@ -0,0 +1,56 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema; + +const activitySchema = new Schema( +{ + type: {type: String}, + username: {type: String}, + timestamp: {type: Date}, + data: {type: Object, _id: false}, +}); + +activitySchema.methods.toObj = function () { + var elem = this.toObject(); + delete elem.__v; + delete elem._id; + return elem; +}; + +const Activity = mongoose.model("Activity", activitySchema); +exports.Activity = Activity; + +// ------------------------------ + +exports.LogActivity = async (type, username, data) => { + var activity_data = { + type: type, + username: username, + timestamp: Date.now(), + data: data + } + try { + const activity = new Activity(activity_data); + return await activity.save(); + } + catch{ + return null; + } +}; + +exports.GetAll = async () => { + try { + const logs = await Activity.find({}); + return logs; + } catch (e) { + return []; + } +}; + +exports.Get = async (data) => { + try { + const logs = await Activity.find(data); + return logs; + } catch (e) { + return []; + } +}; \ No newline at end of file diff --git a/activity/activity.routes.js b/activity/activity.routes.js new file mode 100644 index 0000000..8f63755 --- /dev/null +++ b/activity/activity.routes.js @@ -0,0 +1,19 @@ +const ActivityController = require("./activity.controller"); +const AuthTool = require("../authorization/auth.tool"); +const config = require("../config"); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Middle permission, can read all users +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = function (app) { + +app.get("/activity", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + ActivityController.GetAllActivities, +]); + +} + + diff --git a/authorization/auth.controller.js b/authorization/auth.controller.js new file mode 100644 index 0000000..a0fa56c --- /dev/null +++ b/authorization/auth.controller.js @@ -0,0 +1,94 @@ + +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); +const config = require('../config.js'); +const jwtSecret = config.jwt_secret; +const UserModel = require('../users/users.model'); +const UserTool = require('../users/users.tool'); + +exports.Login = (req, res) => { + try { + + let refreshId = req.login.userId + jwtSecret; + let refresh_key = crypto.randomBytes(16).toString('base64'); + let refresh_hash = crypto.createHmac('sha512', refresh_key).update(refreshId).digest("base64"); + req.login.refresh_key = refresh_key; + + let access_token = jwt.sign(req.login, jwtSecret); + + //Delete some keys for security, empty keys are never valid, also update login time + var update = {refresh_key: refresh_key, proof_key: "", password_recovery_key: "", last_login_time: new Date(), last_online_time: new Date()}; + UserModel.patch(req.login.userId, update); + + var odata = { + id: req.login.userId, + username: req.login.username, + access_token: access_token, + refresh_token: refresh_hash, + permission_level: req.login.permission_level, + validation_level: req.login.validation_level, + duration: config.jwt_expiration, + server_time: new Date(), + version: config.version + } + + return res.status(201).send(odata); + + } catch (err) { + return res.status(500).send({error: err}); + } +}; + +exports.KeepOnline = async(req, res, next) => { + + var token = req.jwt; + UserModel.patch(token.userId, {last_online_time: new Date()}); + + var data = {id: token.userId, username: token.username, login_time: new Date(token.iat * 1000), server_time: new Date() }; + return res.status(200).send(data); +}; + +exports.GetVersion = (req, res) =>{ + return res.status(200).send({version: config.version}); +}; + +// ----- verify user ----------- + +exports.ValidateToken = async(req, res, next) => { + + var token = req.jwt; + var data = {id: token.userId, username: token.username, login_time: new Date(token.iat * 1000), server_time: new Date() }; + return res.status(200).send(data); +}; + +exports.CreateProof = async(req, res) => +{ + var userId = req.jwt.userId; + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found"}); + + user.proof_key = crypto.randomBytes(20).toString('base64'); + await UserModel.save(user); + + return res.status(200).send({proof: user.proof_key}); +} + +exports.ValidateProof = async(req, res) => +{ + var username = req.params.username; + var proof = req.params.proof; + + if(!username || typeof username != "string" || !proof || typeof proof != "string") + return res.status(400).send({error: "Invalid parameters"}); + + var user = await UserModel.getByUsername(username); + if(!user) + return res.status(404).send({error: "User not found"}); + + if(!user.proof_key || user.proof_key != proof) + return res.status(403).send({error: "Invalid Proof"}); + + return res.status(200).send(); +} \ No newline at end of file diff --git a/authorization/auth.routes.js b/authorization/auth.routes.js new file mode 100644 index 0000000..9a23297 --- /dev/null +++ b/authorization/auth.routes.js @@ -0,0 +1,44 @@ + +const AuthController = require('./auth.controller'); +const AuthTool = require('./auth.tool'); + +exports.route = function (app) { + + //Body: username, password + app.post('/auth', app.auth_limiter, [ + AuthTool.isLoginValid, + AuthController.Login + ]); + + //Body: refresh_token + app.post('/auth/refresh', app.auth_limiter, [ + AuthTool.isRefreshValid, + AuthController.Login + ]); + + app.get('/auth/keep',[ + AuthTool.isValidJWT, + AuthController.KeepOnline + ]); + + app.get('/auth/validate',[ + AuthTool.isValidJWT, + AuthController.ValidateToken + ]); + + app.get("/auth/proof/create", [ + AuthTool.isValidJWT, + AuthController.CreateProof + ]); + + app.get("/auth/proof/:username/:proof", [ + AuthTool.isValidJWT, + AuthController.ValidateProof + ]); + + app.get('/version', [ + AuthController.GetVersion + ]); + + +}; \ No newline at end of file diff --git a/authorization/auth.tool.js b/authorization/auth.tool.js new file mode 100644 index 0000000..fee538b --- /dev/null +++ b/authorization/auth.tool.js @@ -0,0 +1,162 @@ +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); + +const config = require('../config.js'); +const UserModel = require('../users/users.model'); + +//-----Validations------ + +var AuthTool = {}; + + +AuthTool.isValidJWT = (req, res, next) => { + + if (!req.headers['authorization']) + return res.status(401).send(); + + try { + //Validate access token + let authorization = req.headers['authorization']; + req.jwt = jwt.verify(authorization, config.jwt_secret); + + //Validate expiry time + const nowSeconds = Math.round(Number(new Date()) / 1000); + const expiration = req.jwt.iat + config.jwt_expiration; + if(nowSeconds > expiration) + return res.status(403).send({error: "Expired"}); + + } catch (err) { + return res.status(403).send({error: "Invalid Token"}); + } + + return next(); +}; + +AuthTool.isLoginValid = async(req, res, next) => { + + if (!req.body || !req.body.password) + return res.status(400).send({error: 'Invalid params'}); + + //Requires EITHER username or email, dont need both + if (!req.body.email && !req.body.username) + return res.status(400).send({error: 'Invalid params'}); + + var user = null; + + if(req.body.email) + user = await UserModel.getByEmail(req.body.email); + else if(req.body.username) + user = await UserModel.getByUsername(req.body.username); + if(!user) + return res.status(404).send({error: "Invalid username or password"}); + + let validPass = AuthTool.validatePassword(user, req.body.password); + if(!validPass) + return res.status(400).send({error: 'Invalid username or password'}); + + if(user.permission_level <= 0) + return res.status(403).send({error: "Your account has been disabled, please contact support!"}); + + req.login = { + userId: user.id, + username: user.username, + email: user.email, + permission_level: user.permission_level, + validation_level: user.validation_level, + provider: req.body.email ? 'email' : 'username', + }; + + return next(); +}; + +AuthTool.isRefreshValid = async(req, res, next) => { + + if (!req.body || !req.body.refresh_token) + return res.status(400).send(); + + if (!req.headers['authorization']) + return res.status(401).send(); + + if (typeof req.body.refresh_token !== "string") + return res.status(400).send(); + + try { + //Validate access token + let authorization = req.headers['authorization']; + req.jwt = jwt.verify(authorization, config.jwt_secret); + + //Validate expiry time + const nowUnixSeconds = Math.round(Number(new Date()) / 1000); + const expiration = req.jwt.iat + config.jwt_refresh_expiration; + if(nowUnixSeconds > expiration) + return res.status(403).send({error: "Token Expired"}); + + //Validate refresh token + let refresh_token = req.body.refresh_token; + let hash = crypto.createHmac('sha512', req.jwt.refresh_key).update(req.jwt.userId + config.jwt_secret).digest("base64"); + if (hash !== refresh_token) + return res.status(403).send({error: 'Invalid refresh token'}); + + //Validate refresh key in DB + var user = await UserModel.getById(req.jwt.userId); + if(!user) + return res.status(404).send({error: "Invalid user"}); + + if(user.refresh_key !== req.jwt.refresh_key) + return res.status(403).send({error: 'Invalid refresh key'}); + + } catch (err) { + return res.status(403).send({error: "Invalid Token"}); + } + + req.login = req.jwt; + delete req.login.iat; //Delete previous iat to generate a new one + return next(); +}; + +AuthTool.hashPassword = (password) => { + let saltNew = crypto.randomBytes(16).toString('base64'); + let hashNew = crypto.createHmac('sha512', saltNew).update(password).digest("base64"); + let newPass = saltNew + "$" + hashNew; + return newPass; +} + +AuthTool.validatePassword = (user, password) => +{ + let passwordFields = user.password.split('$'); + let salt = passwordFields[0]; + let hash = crypto.createHmac('sha512', salt).update(password).digest("base64"); + return hash === passwordFields[1]; +} + +//--- Permisions ----- + +AuthTool.isPermissionLevel = (required_permission) => { + return (req, res, next) => { + let user_permission_level = parseInt(req.jwt.permission_level); + if (user_permission_level >= required_permission) { + return next(); + } else { + return res.status(403).send({error: "Permission Denied"}); + } + }; +}; + +AuthTool.isSameUserOr = (required_permission) => { + return (req, res, next) => { + let user_permission_level = parseInt(req.jwt.permission_level); + let userId = req.params.userId || ""; + let same_user = (req.jwt.userId === userId || req.jwt.username.toLowerCase() === userId.toLowerCase()); + if (userId && same_user) { + return next(); + } else { + if (user_permission_level >= required_permission) { + return next(); + } else { + return res.status(403).send({error: "Permission Denied"}); + } + } + }; +}; + +module.exports = AuthTool; diff --git a/cards/cards.controller.js b/cards/cards.controller.js new file mode 100644 index 0000000..9abf35e --- /dev/null +++ b/cards/cards.controller.js @@ -0,0 +1,134 @@ +const CardModel = require('../cards/cards.model'); +const Activity = require("../activity/activity.model"); +const config = require('../config'); + +exports.AddCard = async(req, res) => +{ + var tid = req.body.tid; + var type = req.body.type; + var team = req.body.team; + var rarity = req.body.rarity || ""; + var mana = req.body.mana || 0; + var attack = req.body.attack || 0; + var hp = req.body.hp || 0; + var cost = req.body.cost || 1; + var packs = req.body.packs || []; + + if(!tid || typeof tid !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!type || typeof type !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!team || typeof team !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!rarity || typeof rarity !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!Number.isInteger(mana) || !Number.isInteger(attack) || !Number.isInteger(hp) || !Number.isInteger(cost)) + return res.status(400).send({ error: "Invalid parameters" }); + + if(packs && !Array.isArray(packs)) + return res.status(400).send({error: "Invalid parameters"}); + + var data = { + tid: tid, + type: type, + team: team, + rarity: rarity, + mana: mana, + attack: attack, + hp: hp, + cost: cost, + packs: packs, + } + + //Update or create + var card = await CardModel.get(tid); + if(card) + card = await CardModel.update(card, data); + else + card = await CardModel.create(data); + + if(!card) + return res.status(500).send({error: "Error updating card"}); + + return res.status(200).send(data); +}; + +exports.AddCardList = async(req, res) => +{ + var cards = req.body.cards; + if(!Array.isArray(cards)) + return res.status(400).send({error: "Invalid parameters"}); + + var ocards = []; + for(var i=0; i +{ + CardModel.remove(req.params.tid); + return res.status(204).send({}); +}; + +exports.DeleteAll = async(req, res) => +{ + CardModel.removeAll(); + return res.status(204).send({}); +}; + +exports.GetCard = async(req, res) => +{ + var tid = req.params.tid; + + if(!tid) + return res.status(400).send({error: "Invalid parameters"}); + + var card = await CardModel.get(tid); + if(!card) + return res.status(404).send({error: "Card not found: " + tid}); + + return res.status(200).send(card.toObj()); +}; + +exports.GetAll = async(req, res) => +{ + var cards = await CardModel.getAll(); + + for(var i=0; i { + try{ + var card = await Card.findOne({tid: tid}); + return card; + } + catch{ + return null; + } +}; + +exports.getAll = async(filter) => { + + try{ + filter = filter || {}; + var cards = await Card.find(filter); + return cards || []; + } + catch{ + return []; + } +}; + +exports.getByPack = async(packId, filter) => { + + try{ + filter = filter || {}; + if(packId) + { + filter.packs = {$in:[packId]}; + } + var cards = await Card.find(filter); + return cards || []; + } + catch{ + return []; + } +}; + +exports.create = async(data) => { + try{ + var card = new Card(data); + return await card.save(); + } + catch{ + return null; + } +}; + +exports.update = async(card, data) => { + + try{ + if(!card) return null; + + for (let i in data) { + card[i] = data[i]; + card.markModified(i); + } + + var updated = await card.save(); + return updated; + } + catch{ + return null; + } +}; + +exports.remove = async(tid) => { + try{ + var result = await Card.deleteOne({tid: tid}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; + +exports.removeAll = async() => { + try{ + var result = await Card.deleteMany({}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; \ No newline at end of file diff --git a/cards/cards.routes.js b/cards/cards.routes.js new file mode 100644 index 0000000..e5e2e30 --- /dev/null +++ b/cards/cards.routes.js @@ -0,0 +1,42 @@ +const CardController = require('./cards.controller'); +const AuthTool = require('../authorization/auth.tool'); +const config = require('../config'); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Higher permission, can read all users +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = function (app) { + + app.get('/cards/:tid', [ + CardController.GetCard + ]); + + app.get('/cards', [ + CardController.GetAll + ]); + + app.post('/cards/add', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + CardController.AddCard + ]); + + app.post('/cards/add/list', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + CardController.AddCardList + ]); + + app.delete("/cards/:tid", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + CardController.DeleteCard + ]); + + app.delete("/cards", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + CardController.DeleteAll + ]); +}; \ No newline at end of file diff --git a/cards/cards.tool.js b/cards/cards.tool.js new file mode 100644 index 0000000..c112be5 --- /dev/null +++ b/cards/cards.tool.js @@ -0,0 +1,111 @@ +const config = require('../config.js'); +const crypto = require('crypto'); +const CardModel = require('../cards/cards.model'); + +const CardTool = {}; + +CardTool.getPackCards = async(pack) => +{ + var pack_cards = await CardModel.getByPack(pack.tid); + + var cards = []; + for(var i=0; i +{ + var rarities = is_first ? pack.rarities_1st : pack.rarities; + if(!rarities || rarities.length == 0) + return ""; //Any rarity + + var total = 0; + for(var rarity of rarities) { + total += rarity.value; + } + + var rvalue = Math.floor(Math.random()*total); + + for(var i=0; i +{ + var variants = pack.variants; + if(!variants || variants.length == 0) + return ""; + + var total = 0; + for(var variant of variants) { + total += variant.value; + } + + var rvalue = Math.floor(Math.random()*total); + + for(var i=0; i +{ + var valid_cards = []; + for(var i=0; i +{ + if(all_cards.length > 0) + { + var card = all_cards[Math.floor(Math.random()*all_cards.length)]; + return card; + } + return null; +}; + +module.exports = CardTool; \ No newline at end of file diff --git a/config.js b/config.js new file mode 100644 index 0000000..61756e0 --- /dev/null +++ b/config.js @@ -0,0 +1,73 @@ +module.exports = { + version: "1.13", + + port: 80, + port_https: 443, + api_title: "TCG Engine API", //Display name + api_url: "", //If you set the URL, will block all direct IP access, or wrong url access, leave blank to allow all url access + + //HTTPS config, certificate is required if you want to enable HTTPS + https_key: "/etc/letsencrypt/live/yoursite.com/privkey.pem", + https_ca: "/etc/letsencrypt/live/yoursite.com/chain.pem", + https_cert: "/etc/letsencrypt/live/yoursite.com/cert.pem", + allow_http: true, + allow_https: false, + + //JS Web Token Config + jwt_secret: "JWT_123456789", //Change this to a unique secret value + jwt_expiration: 3600 * 10, //In seconds (10 hours) + jwt_refresh_expiration: 3600 * 100, //In seconds (100 hours) + + //User Permissions Config + permissions: { + USER: 1, + SERVER: 5, + ADMIN: 10, + }, + + //Mongo Connection + mongo_user: "", + mongo_pass: "", + mongo_host: "127.0.0.1", + mongo_port: "27017", + mongo_db: "tcgengine", + + //Limiter to protect from DDOS, will block IP that do too many requests + limiter_window: 1000 * 120, //in ms, will reset the counts after this time + limiter_max: 500, //max nb of GET requests within the time window + limiter_post_max: 100, //max nb of POST requests within the time window + limiter_auth_max: 10, //max nb of Login/Register request within the time window + limiter_proxy: false, //Must be set to true if your server is behind a proxy, otherwise the proxy itself will be blocked + + ip_whitelist: ["127.0.0.1"], //These IP are not affected by the limiter, for example you could add your game server's IP + ip_blacklist: [], //These IP are blocked forever + + //Email config, required for the API to send emails + smtp_enabled: false, + smtp_name: "TCG Engine", //Name of sender in emails + smtp_email: "", //Email used to send + smtp_server: "", //SMTP server URL + smtp_port: "465", + smtp_user: "", //SMTP auth user + smtp_password: "", //SMTP auth password + + //ELO settings + elo_k: 32, //Higher K number will affect elo more each match + elo_ini_k: 128, //K value for the first X matches can be higher + elo_ini_match: 5, //X number of match for the previous value + + //New Users + start_coins: 5000, + start_elo: 1000, + + //Match Rewards + coins_victory: 200, //Victory coins reward + coins_defeat: 100, //Defeat coins reward + xp_victory: 100, //Victory xp reward + xp_defeat: 50, //Defeat xp reward + + //Market + sell_ratio: 0.8, //Sell ratio compared to buy price + avatar_cost: 500, + cardback_cost: 1000, +}; diff --git a/decks/decks.controller.js b/decks/decks.controller.js new file mode 100644 index 0000000..6f00ca3 --- /dev/null +++ b/decks/decks.controller.js @@ -0,0 +1,75 @@ +const DeckModel = require('../decks/decks.model'); +const Activity = require("../activity/activity.model"); +const config = require('../config'); + +exports.AddDeck = async(req, res) => +{ + var tid = req.body.tid; + var title = req.body.title; + var hero = req.body.hero; + var cards = req.body.cards; + + if(!tid || typeof tid !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!title || typeof title !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!hero || typeof hero !== "object") + return res.status(400).send({error: "Invalid parameters"}); + + if(cards && !Array.isArray(cards)) + return res.status(400).send({error: "Invalid parameters"}); + + var deck_data = { + tid: tid, + title: title, + hero: hero, + cards: cards || [], + } + + //Update or create + var deck = await DeckModel.get(tid); + if(deck) + deck = await DeckModel.update(deck, deck_data); + else + deck = await DeckModel.create(deck_data); + + if(!deck) + res.status(500).send({error: "Error updating deck"}); + + return res.status(200).send(deck); +}; + +exports.DeleteDeck = async(req, res) => { + DeckModel.remove(req.params.tid); + return res.status(204).send({}); +}; + +exports.DeleteAll = async(req, res) => { + DeckModel.removeAll(); + return res.status(204).send({}); +}; + +exports.GetDeck = async(req, res) => +{ + var deckId = req.params.tid; + + if(!deckId) + return res.status(400).send({error: "Invalid parameters"}); + + var deck = await DeckModel.get(deckId); + if(!deck) + return res.status(404).send({error: "Deck not found: " + deckId}); + + return res.status(200).send(deck.toObj()); +}; + + +exports.GetAll = async(req, res) => +{ + var decks = await DeckModel.getAll(); + return res.status(200).send(decks); +}; + + diff --git a/decks/decks.model.js b/decks/decks.model.js new file mode 100644 index 0000000..94b5b85 --- /dev/null +++ b/decks/decks.model.js @@ -0,0 +1,99 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const deckSchema = new Schema({ + + tid: { type: String, index: true, unique: true, default: "" }, + title: { type: String, default: "" }, + hero: {tid: String, variant: String, _id: false}, + cards: [{tid: String, variant: String, quantity: Number, _id: false}], + +}); + +deckSchema.methods.toObj = function() { + var deck = this.toObject(); + delete deck.__v; + delete deck._id; + return deck; +}; + +const Deck = mongoose.model('Decks', deckSchema); + +exports.get = async(deckId) => { + try{ + var deck = await Deck.findOne({tid: deckId}); + return deck; + } + catch{ + return null; + } +}; + +exports.getList = async(decks_tids) => { + try{ + var decks = await Deck.find({tid: { $in: decks_tids } }); + return decks || []; + } + catch{ + return []; + } +}; + +exports.getAll = async() => { + + try{ + var decks = await Deck.find() + return decks || []; + } + catch{ + return []; + } +}; + +exports.create = async(data) => { + try{ + var deck = new Deck(data); + return await deck.save(); + } + catch{ + return null; + } +}; + +exports.update = async(deck, data) => { + + try{ + if(!deck) return null; + + for (let i in data) { + deck[i] = data[i]; + deck.markModified(i); + } + + var updated = await deck.save(); + return updated; + } + catch{ + return null; + } +}; + +exports.remove = async(deckId) => { + try{ + var result = await Deck.deleteOne({tid: deckId}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; + +exports.removeAll = async() => { + try{ + var result = await Deck.deleteMany({}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; diff --git a/decks/decks.routes.js b/decks/decks.routes.js new file mode 100644 index 0000000..2c48452 --- /dev/null +++ b/decks/decks.routes.js @@ -0,0 +1,36 @@ +const DeckController = require('./decks.controller'); +const AuthTool = require('../authorization/auth.tool'); +const config = require('../config'); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Higher permission, can read all users +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = function (app) { + + app.get('/decks/:tid', [ + DeckController.GetDeck + ]); + + app.get('/decks', [ + DeckController.GetAll + ]); + + app.post('/decks/add', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + DeckController.AddDeck + ]); + + app.delete("/decks/:tid", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + DeckController.DeleteDeck + ]); + + app.delete("/decks", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + DeckController.DeleteAll + ]); +}; \ No newline at end of file diff --git a/jobs/jobs.js b/jobs/jobs.js new file mode 100644 index 0000000..8a5b556 --- /dev/null +++ b/jobs/jobs.js @@ -0,0 +1,21 @@ +const schedule = require('node-schedule'); + +const ExecuteJobs = async() => +{ + //console.log('Run Hourly Jobs.....'); + + //Add custom hourly jobs here + + + +}; + +exports.InitJobs = function() +{ + schedule.scheduleJob('* 1 * * *', function(){ // this for one hour + ExecuteJobs(); + }); + + //Test run when starting + ExecuteJobs(); +} \ No newline at end of file diff --git a/market/market.controller.js b/market/market.controller.js new file mode 100644 index 0000000..28957cc --- /dev/null +++ b/market/market.controller.js @@ -0,0 +1,215 @@ +const UserModel = require('../users/users.model'); +const MarketModel = require('./market.model'); +const UserTool = require('../users/users.tool'); +const DateTool = require('../tools/date.tool'); +const Activity = require("../activity/activity.model"); +const config = require('../config'); + +exports.addOffer = async(req, res) => { + + var username = req.jwt.username; + var card_tid = req.body.card; + var variant = req.body.variant; + var quantity = req.body.quantity; + var price = req.body.price; + + //Validate params + if (!username || !card_tid || !variant || !quantity || !price) + return res.status(400).send({ error: "Invalid parameters" }); + + if(typeof username !== "string"|| typeof quantity !== "number" || typeof price !== "number" || typeof card_tid !== "string" || typeof variant !== "string" ) + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Number.isInteger(quantity) || !Number.isInteger(price) || price <= 0 || quantity <= 0) + return res.status(400).send({ error: "Invalid parameters" }); + + //Get user + var user = await UserModel.getByUsername(username); + if (!user) + return res.status(404).send({ error: "Can't find user " + username }); + + if(!UserTool.hasCard(user, card_tid, variant, quantity)) + return res.status(400).send({ error: "You don't have those cards!" }); + + //Offer + var offer = { + seller: username, + card: card_tid, + variant: variant, + quantity: quantity, + price: price, + } + + //Remove card from user + var removeCards = [{tid: card_tid, variant: variant, quantity: -quantity}]; + var addSucc = await UserTool.addCards(user, removeCards); + if(!addSucc) + return res.status(500).send({ error: "Error removing cards from user " + username }); + + //Update database + var uOffer = await MarketModel.add(username, card_tid, variant, offer); + var uUser = await UserModel.update(user, { cards: user.cards, }); + + if(!uUser || !uOffer) + return res.status(500).send({ error: "Error creating market offer " + username }); + + //Activity + //var act = await Activity.LogActivity("market_add", req.jwt.username, uOffer.toObj()); + //if (!act) return res.status(500).send({ error: "Failed to log activity!" }); + + return res.status(200).send(uOffer.toObj()); +}; + +exports.removeOffer = async(req, res) => { + + var username = req.jwt.username; + var card_tid = req.body.card; + var variant = req.body.variant; + + //Validate params + if (!username || !card_tid || !variant) + return res.status(400).send({ error: "Invalid parameters" }); + + if(typeof username !== "string"|| typeof card_tid !== "string" || typeof variant !== "string" ) + return res.status(400).send({ error: "Invalid parameters" }); + + var user = await UserModel.getByUsername(username); + if (!user) + return res.status(404).send({ error: "Can't find user " + username }); + + var offer = await MarketModel.getOffer(username, card_tid, variant) + if (!offer) + return res.status(404).send({ error: "No market offer for " + username + " " + card_tid }); + + //Add cards user + var addCards = [{tid: card_tid, variant: variant, quantity: offer.quantity}]; + var addSucc = await UserTool.addCards(user, addCards); + if(!addSucc) + return res.status(500).send({ error: "Error adding cards to user " + username }); + + //Update database + var uUser = await UserModel.update(user, { cards: user.cards }); + var uOffer = await MarketModel.remove(username, card_tid, variant); + + if(!uUser || !uOffer) + return res.status(500).send({ error: "Error removing market offer " + username }); + + //Activity + //var act = await Activity.LogActivity("market_remove", req.jwt.username, {}); + //if (!act) return res.status(500).send({ error: "Failed to log activity!" }); + + return res.status(200).send({success: uOffer}); +}; + +exports.trade = async(req, res) => { + + var username = req.jwt.username; + var seller_user = req.body.seller; + var card_tid = req.body.card; + var variant = req.body.variant; + var quantity = req.body.quantity; + + //Validate params + if (!username || !seller_user || !card_tid || !variant || !quantity) + return res.status(400).send({ error: "Invalid parameters" }); + + if(typeof seller_user !== "string" || typeof card_tid !== "string" || typeof variant !== "string" || typeof quantity !== "number") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Number.isInteger(quantity) || quantity <= 0) + return res.status(400).send({ error: "Invalid parameters" }); + + //Get user + var user = await UserModel.getByUsername(username); + var seller = await UserModel.getByUsername(seller_user); + if (!user || !seller) + return res.status(404).send({ error: "Can't find user " + username + " or " + seller_user }); + + if(user.id == seller.id) + return res.status(403).send({ error: "Can't trade with yourself!" }); + + //Get offer + var offer = await MarketModel.getOffer(seller_user, card_tid, variant) + if (!offer) + return res.status(404).send({ error: "No market offer for " + seller_user + " " + card_tid }); + + var value = quantity * offer.price; + if(user.coins < value) + return res.status(403).send({ error: "Not enough coins to trade!" }); + if(quantity > offer.quantity) + return res.status(403).send({ error: "Not enough cards to trade!" }); + + //Add cards and coins + var addCards = [{tid: card_tid, variant: variant, quantity: quantity}]; + var addSucc = await UserTool.addCards(user, addCards); + if(!addSucc) + return res.status(500).send({ error: "Error adding cards to user " + username }); + + user.coins -= value; + seller.coins += value; + + //Update database + var uUser = await UserModel.update(user, { coins: user.coins, cards: user.cards }); + var uSeller = await UserModel.update(seller, { coins: seller.coins }); + var uOffer = await MarketModel.reduce(seller_user, card_tid, variant, quantity); + if(!uUser || !uOffer || !uSeller) + return res.status(500).send({ error: "Error trading market offer " + username + " " + seller_user }); + + //Activity + var aData = {buyer: username, seller: seller_user, card: card_tid, quantity: quantity, price: offer.price }; + var act = await Activity.LogActivity("market_trade", req.jwt.username, aData); + if (!act) return res.status(500).send({ error: "Failed to log activity!" }); + + return res.status(200).send(aData); +}; + +exports.getBySeller = async(req, res) => { + + if(!req.params.username) + return res.status(400).send({ error: "Invalid parameters" }); + + var list = await MarketModel.getBySeller(req.params.username); + for(var i=0; i { + + var tid = req.params.tid; + var variant = req.params.variant; + + if(!tid || !variant) + return res.status(400).send({ error: "Invalid parameters" }); + + var list = await MarketModel.getByCard(tid, variant); + for(var i=0; i { + + var tid = req.params.tid; + var variant = req.params.variant; + var username = req.params.username; + + if(!tid || !variant || !username) + return res.status(400).send({ error: "Invalid parameters" }); + + var offer = await MarketModel.getOffer(username, tid, variant); + if(!offer) + return res.status(404).send({ error: "Offer not found" }); + + return res.status(200).send(offer.toObj()); +}; + +exports.getAll = async(req, res) => { + var list = await MarketModel.getAll(); + for(var i=0; i { + + try{ + var regex = new RegExp(["^", user, "$"].join(""), "i"); + var offer = await Market.findOne({seller: regex, card: card_tid, variant: variant_id}); + return offer; + } + catch{ + return null; + } +}; + +exports.getBySeller = async(user) => { + + try{ + var regex = new RegExp(["^", user, "$"].join(""), "i"); + var offers = await Market.find({seller: regex}); + offers = offers || []; + return offers; + } + catch{ + return []; + } +}; + +exports.getByCard = async(card_tid, variant_id) => { + + try{ + var offers = await Market.find({card: card_tid, variant: variant_id}); + offers = offers || []; + return offers; + } + catch{ + return []; + } +}; + +exports.getAll = async() => { + + try{ + var offers = await Market.find() + offers = offers || []; + return offers; + } + catch{ + return []; + } +}; + +exports.getAllLimit = async(perPage, page) => { + + try{ + var offers = await Market.find().limit(perPage).skip(perPage * page) + offers = offers || []; + return offers; + } + catch{ + return []; + } +}; + +exports.add = async(user, card, variant, data) => { + + try{ + var offer = await Market.findOne({seller: user, card: card, variant: variant}); + + if(!offer) + { + offer = new Market(data); + offer.date = Date.now(); + return await offer.save(); + } + else + { + offer.quantity += data.quantity; + offer.price = data.price; + offer.date = Date.now(); + + var updated = await offer.save(); + return updated; + } + } + catch{ + return null; + } +}; + + +exports.reduce = async(user, card, variant, quantity) => { + + try{ + var offer = await Market.findOne({seller: user, card: card, variant: variant}); + if(offer) + { + offer.quantity -= quantity; + if(offer.quantity > 0) + { + var updated = await offer.save(); + return updated; + } + else{ + var result = await Market.deleteOne({seller: user, card: card}); + return result && result.deletedCount > 0; + } + } + } + catch{ + return null; + } +}; + +exports.remove = async(user, card, variant) => { + + try{ + var result = await Market.deleteOne({seller: user, card: card, variant: variant}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; diff --git a/market/market.routes.js b/market/market.routes.js new file mode 100644 index 0000000..0a86b93 --- /dev/null +++ b/market/market.routes.js @@ -0,0 +1,51 @@ +const MarketController = require('./market.controller'); +const AuthTool = require('../authorization/auth.tool'); +const config = require('../config'); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Middle permission, can read all users and grant rewards +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = function (app) { + + app.post("/market/cards/add", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MarketController.addOffer, + ]); + app.post("/market/cards/remove", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MarketController.removeOffer, + ]); + app.post("/market/cards/trade", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MarketController.trade, + ]); + + app.get("/market/cards/", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MarketController.getAll, + ]); + + app.get("/market/cards/user/:username", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MarketController.getBySeller, + ]); + + app.get("/market/cards/card/:tid", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MarketController.getByCard, + ]); + + app.get("/market/cards/offer/:username/:tid", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MarketController.getOffer, + ]); + +}; \ No newline at end of file diff --git a/matches/matches.controller.js b/matches/matches.controller.js new file mode 100644 index 0000000..ac1c4ee --- /dev/null +++ b/matches/matches.controller.js @@ -0,0 +1,123 @@ +const MatchModel = require('./matches.model'); +const MatchTool = require('./matches.tool'); +const UserModel = require('../users/users.model'); +const DateTool = require('../tools/date.tool'); +const config = require('../config'); + +exports.addMatch = async(req, res) => { + + var tid = req.body.tid; + var players = req.body.players; + var ranked = req.body.ranked ? true : false; + var mode = req.body.mode || ""; + + if (!tid || !players || !Array.isArray(players) || players.length != 2) + return res.status(400).send({ error: "Invalid parameters" }); + + if (mode && typeof mode !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + var fmatch = await MatchModel.get(tid); + if(fmatch) + return res.status(400).send({error:"Match already exists: " + tid}); + + var player0 = await UserModel.getByUsername(players[0]); + var player1 = await UserModel.getByUsername(players[1]); + if(!player0 || !player1) + return res.status(404).send({error:"Can't find players"}); + + if(player0.id == player1.id) + return res.status(400).send({error:"Can't play against yourself"}); + + var match = {}; + match.tid = tid; + match.players = players; + match.winner = ""; + match.completed = false; + match.ranked = ranked; + match.mode = mode; + match.start = Date.now(); + match.end = Date.now(); + match.udata = []; + match.udata.push(MatchTool.GetPlayerData(player0)); + match.udata.push(MatchTool.GetPlayerData(player1)); + + var match = await MatchModel.create(match); + if(!match) + return res.status(500).send({error:"Unable to create match"}); + + res.status(200).send(match); + +}; + +exports.completeMatch = async(req, res) => { + + var matchId = req.body.tid; + var winner = req.body.winner; + + if (!matchId || !winner) + return res.status(400).send({ error: "Invalid parameters" }); + + if(typeof matchId != "string" || typeof winner != "string") + return res.status(400).send({error: "Invalid parameters" }); + + var match = await MatchModel.get(matchId); + if(!match) + return res.status(404).send({error: "Match not found"}); + + if(match.completed) + return res.status(400).send({error: "Match already completed"}); + + var player0 = await UserModel.getByUsername(match.players[0]); + var player1 = await UserModel.getByUsername(match.players[1]); + if(!player0 || !player1) + return res.status(404).send({error:"Can't find players"}); + + match.end = Date.now(); + match.winner = winner; + match.completed = true; + + //Add Rewards + if(match.ranked) + { + match.udata[0].reward = await MatchTool.GainMatchReward(player0, player1, winner); + match.udata[1].reward = await MatchTool.GainMatchReward(player1, player0, winner); + match.markModified('udata'); + } + + //Save match + var uMatch = await match.save(); + + //Return + res.status(200).send(uMatch); +}; + +exports.getAll = async(req, res) => { + + var start = req.query.start ? DateTool.tagToDate(req.query.start) : null; + var end = req.query.end ? DateTool.tagToDate(req.query.end) : null; + + var matches = await MatchModel.list(start, end); + if(!matches) + return res.status(400).send({error: "Invalid Parameters"}); + + return res.status(200).send(matches); +}; + +exports.getByTid = async(req, res) => { + + var match = await MatchModel.get(req.params.tid); + if(!match) + return res.status(404).send({error: "Match not found " + req.params.tid}); + + return res.status(200).send(match); +}; + +exports.getLatest = async(req, res) => { + + var match = await MatchModel.getLast(req.params.userId); + if(!match) + return res.status(404).send({error: "Match not found for user " + req.params.userId}); + + return res.status(200).send(match); +}; diff --git a/matches/matches.model.js b/matches/matches.model.js new file mode 100644 index 0000000..93732c8 --- /dev/null +++ b/matches/matches.model.js @@ -0,0 +1,87 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const matchSchema = new Schema({ + + tid: { type: String, index: true, default: "" }, + players: [{type: String, default: []}], + winner: {type: String, default: ""}, + completed: {type: Boolean, default: false}, + ranked: {type: Boolean, default: false}, + mode: { type: String, default: "" }, + + start: {type: Date, default: null}, + end: {type: Date, default: null}, + + udata: [{ type: Object, _id: false }], +}); + +matchSchema.methods.toObj = function() { + var match = this.toObject(); + delete match.__v; + delete match._id; + return match; +}; + +const Match = mongoose.model('Matches', matchSchema); + +exports.get = async(matchId) => { + try{ + var match = await Match.findOne({tid: matchId}); + return match; + } + catch{ + return null; + } +}; + +exports.getAll = async() => { + + try{ + var matches = await Match.find() + return matches || []; + } + catch{ + return []; + } +}; + +exports.create = async(matchData) => { + const match = new Match(matchData); + return await match.save(); +}; + +exports.list = async(startTime, endTime, winnerId, completed) => { + + startTime = startTime || new Date(-8640000000000000); + endTime = endTime || new Date(8640000000000000); + + var options = {}; + + if(startTime && endTime) + options.end = { $gte: startTime, $lte: endTime }; + + if(winnerId) + options.players = winnerId; + + if(completed) + options.completed = true; + + try{ + var matches = await Match.find(options) + return matches || []; + } + catch{ + return []; + } +}; + +exports.remove = async(matchId) => { + try{ + var result = await Match.deleteOne({tid: matchId}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; diff --git a/matches/matches.routes.js b/matches/matches.routes.js new file mode 100644 index 0000000..4e94d6a --- /dev/null +++ b/matches/matches.routes.js @@ -0,0 +1,34 @@ +const MatchesController = require('./matches.controller'); +const MatchesTool = require('./matches.tool'); +const AuthTool = require('../authorization/auth.tool'); +const config = require('../config'); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Higher permission, can read all users +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = function (app) { + + app.post('/matches/add', app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(SERVER), + MatchesController.addMatch + ]); + app.post('/matches/complete', app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(SERVER), + MatchesController.completeMatch + ]); + + //-- Getter + app.get('/matches', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(SERVER), + MatchesController.getAll + ]); + app.get('/matches/:tid', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + MatchesController.getByTid + ]); +}; \ No newline at end of file diff --git a/matches/matches.tool.js b/matches/matches.tool.js new file mode 100644 index 0000000..1ae3953 --- /dev/null +++ b/matches/matches.tool.js @@ -0,0 +1,71 @@ +const UserTool = require('../users/users.tool'); +const config = require('../config.js'); + + +var MatchTool = {}; + + +MatchTool.calculateELO = (player_elo, opponent_elo, progress, won, lost) => +{ + var p_elo = player_elo || 1000; + var o_elo = opponent_elo || 1000; + + var p_elo_log = Math.pow(10.0, p_elo / 400.0); + var o_elo_log = Math.pow(10.0, o_elo / 400.0); + var p_expected = p_elo_log / (p_elo_log + o_elo_log); + var p_score = won ? 1.0 : (lost ? 0.0 : 0.5); + + progress = Math.min(Math.max(progress, 0.0), 1.0); + var elo_k = progress * config.elo_k + (1.0 - progress) * config.elo_ini_k; + var new_elo = Math.round(p_elo + elo_k * (p_score - p_expected)); + return new_elo; +} + +MatchTool.GetPlayerData = (player) => +{ + var data = {}; + data.username = player.username; + data.elo = player.elo; + data.reward = {}; + return data; +} + +MatchTool.GainMatchReward = async(player, opponent, winner_username) => { + + var player_elo = player.elo; + var opponent_elo = opponent.elo; + var won = winner_username == player.username; + var lost = winner_username == opponent.username; + + //Rewards + var xp = won ? config.xp_victory : config.xp_defeat; + var coins = won ? config.coins_victory : config.coins_defeat; + + player.xp += xp; + player.coins += coins; + + //Match winrate + player.matches +=1; + + if(won) + player.victories += 1; + else if (lost) + player.defeats += 1; + + //Calculate elo + var match_count = player.matches || 0; + var match_progress = Math.min(Math.max(match_count / config.elo_ini_match, 0.0), 1.0); + var new_elo = MatchTool.calculateELO(player_elo, opponent_elo, match_progress, won, lost); + player.elo = new_elo; + player.save(); + + var reward = { + elo: player.elo, + xp: xp, + coins: coins + }; + + return reward; +}; + +module.exports = MatchTool; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e2729eb --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "tcg-engine-api", + "version": "0.0.1", + "description": "", + "main": "server.js", + "author": "IndieMarc", + "dependencies": { + "express": "^4.18.2", + "express-rate-limit": "^6.7.0", + "express-slow-down": "^1.5.0", + "jsonwebtoken": "^9.0.0", + "mongoose": "^6.8.4", + "node-schedule": "^2.1.1", + "nodemailer": "^6.9.0" + } +} diff --git a/packs/packs.controller.js b/packs/packs.controller.js new file mode 100644 index 0000000..e06a3d3 --- /dev/null +++ b/packs/packs.controller.js @@ -0,0 +1,80 @@ +const PackModel = require("./packs.model"); + +exports.AddPack = async(req, res) => +{ + var tid = req.body.tid; + var cards = req.body.cards || 1; + var cost = req.body.cost || 1; + var random = req.body.random || false; + var rarities_1st = req.body.rarities_1st || []; + var rarities = req.body.rarities || []; + var variants = req.body.variants || []; + + if(!tid || typeof tid !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!Number.isInteger(cards) || !Number.isInteger(cost)) + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Array.isArray(rarities_1st) || !Array.isArray(rarities) || !Array.isArray(variants)) + return res.status(400).send({error: "Invalid parameters"}); + + var data = { + tid: tid, + cards: cards, + cost: cost, + random: random, + rarities_1st: rarities_1st, + rarities: rarities, + variants: variants, + } + + //Update or create + var pack = await PackModel.get(tid); + if(pack) + pack = await PackModel.update(pack, data); + else + pack = await PackModel.create(data); + + if(!pack) + return res.status(500).send({error: "Error updating pack"}); + + return res.status(200).send(data); +}; + +exports.DeletePack = async(req, res) => +{ + PackModel.remove(req.params.tid); + return res.status(204).send({}); +}; + +exports.DeleteAll = async(req, res) => +{ + PackModel.removeAll(); + return res.status(204).send({}); +}; + +exports.GetPack = async(req, res) => +{ + var tid = req.params.tid; + + if(!tid) + return res.status(400).send({error: "Invalid parameters"}); + + var pack = await PackModel.get(tid); + if(!pack) + return res.status(404).send({error: "Pack not found: " + tid}); + + return res.status(200).send(pack.toObj()); +}; + +exports.GetAll = async(req, res) => +{ + var packs = await PackModel.getAll(); + + for(var i=0; i { + + try{ + var pack = new Pack(data); + return await pack.save(); + } + catch{ + return null; + } +}; + +exports.get = async(set_tid) => { + + try{ + var pack = await Pack.findOne({tid: set_tid}); + return pack; + } + catch{ + return null; + } +}; + +exports.getAll = async() => { + + try{ + var packs = await Pack.find({}); + return packs; + } + catch{ + return []; + } + +}; + +exports.update = async(pack, data) => { + + try{ + if(!pack) return null; + + for (let i in data) { + pack[i] = data[i]; + pack.markModified(i); + } + + var updated = await pack.save(); + return updated; + } + catch{ + return null; + } +}; + +exports.remove = async(pack_tid) => { + + try{ + var result = await Pack.deleteOne({tid: pack_tid}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; + +exports.removeAll = async() => { + try{ + var result = await Pack.deleteMany({}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; \ No newline at end of file diff --git a/packs/packs.routes.js b/packs/packs.routes.js new file mode 100644 index 0000000..be940d3 --- /dev/null +++ b/packs/packs.routes.js @@ -0,0 +1,36 @@ +const config = require("../config"); +const PacksController = require("./packs.controller"); +const AuthTool = require("../authorization/auth.tool"); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Higher permission, can read all users +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = (app) => { + + app.get("/packs", [ + PacksController.GetAll + ]); + + app.get("/packs/:tid", [ + PacksController.GetPack + ]); + + app.post("/packs/add", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + PacksController.AddPack + ]); + + app.delete("/packs/:tid", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + PacksController.DeletePack + ]); + + app.delete("/packs", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + PacksController.DeleteAll + ]); +}; diff --git a/rewards/rewards.controller.js b/rewards/rewards.controller.js new file mode 100644 index 0000000..46165d2 --- /dev/null +++ b/rewards/rewards.controller.js @@ -0,0 +1,99 @@ +const RewardModel = require('../rewards/rewards.model'); +const Activity = require("../activity/activity.model"); +const config = require('../config'); + +exports.AddReward = async(req, res) => +{ + var rewardId = req.body.tid; + var group = req.body.group; + var repeat = req.body.repeat; + var xp = req.body.xp; + var coins = req.body.coins; + var cards = req.body.cards; + var packs = req.body.packs; + var decks = req.body.decks; + var avatars = req.body.avatars; + var cardbacks = req.body.cardbacks; + + if(!rewardId || typeof rewardId !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(group && typeof group !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(xp && !Number.isInteger(xp)) + return res.status(400).send({error: "Invalid parameters"}); + if(coins && !Number.isInteger(coins)) + return res.status(400).send({error: "Invalid parameters"}); + if(cards && !Array.isArray(cards)) + return res.status(400).send({error: "Invalid parameters"}); + if(packs && !Array.isArray(packs)) + return res.status(400).send({error: "Invalid parameters"}); + if(decks && !Array.isArray(decks)) + return res.status(400).send({error: "Invalid parameters"}); + if(avatars && !Array.isArray(avatars)) + return res.status(400).send({error: "Invalid parameters"}); + if(cardbacks && !Array.isArray(cardbacks)) + return res.status(400).send({error: "Invalid parameters"}); + + var reward_data = { + tid: rewardId, + group: group || "", + repeat: repeat || false, + xp: xp || 0, + coins: coins || 0, + cards: cards || [], + packs: packs || [], + decks: decks || [], + avatars: avatars || [], + cardbacks: cardbacks || [], + } + + //Update or create + var reward = await RewardModel.get(rewardId); + if(reward) + reward = await RewardModel.update(reward, reward_data); + else + reward = await RewardModel.create(reward_data); + + if(!reward) + res.status(500).send({error: "Error updating reward"}); + + //Activity + const act = await Activity.LogActivity("reward_add", req.jwt.username, reward); + if (!act) return res.status(500).send({ error: "Failed to log activity!" }); + + return res.status(200).send(reward); +}; + +exports.DeleteReward = async(req, res) => { + RewardModel.remove(req.params.tid); + return res.status(204).send({}); +}; + +exports.DeleteAll = async(req, res) => { + RewardModel.removeAll(); + return res.status(204).send({}); +}; + +exports.GetReward = async(req, res) => +{ + var rewardTid = req.params.tid; + + if(!rewardTid) + return res.status(400).send({error: "Invalid parameters"}); + + var reward = await RewardModel.get(rewardTid); + if(!reward) + return res.status(404).send({error: "Reward not found: " + rewardTid}); + + return res.status(200).send(reward.toObj()); +}; + +exports.GetAll = async(req, res) => +{ + var rewards = await RewardModel.getAll(); + return res.status(200).send(rewards); +}; + + diff --git a/rewards/rewards.model.js b/rewards/rewards.model.js new file mode 100644 index 0000000..d158723 --- /dev/null +++ b/rewards/rewards.model.js @@ -0,0 +1,107 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const rewardSchema = new Schema({ + + tid: { type: String, index: true, unique: true, default: "" }, + group: { type: String, index: true, default: "" }, + repeat: { type : Boolean, default: false }, //If true, can be gained multiple times but only server/admin can grant it + + xp: { type: Number, default: 0 }, + coins: { type: Number, default: 0 }, + cards: [{type: String}], + packs: [{type: String}], + decks: [{type: String}], + avatars: [{type: String}], + cardbacks: [{type: String}], + +}); + +rewardSchema.methods.toObj = function() { + var reward = this.toObject(); + delete reward.__v; + delete reward._id; + return reward; +}; + +const Reward = mongoose.model('Rewards', rewardSchema); + +exports.get = async(rewardId) => { + try{ + var reward = await Reward.findOne({tid: rewardId}); + return reward; + } + catch{ + return null; + } +}; + +exports.getGroup = async(group) => { + + try{ + var rewards = await Reward.find({group: group}) + return rewards || []; + } + catch{ + return []; + } +}; + +exports.getAll = async() => { + + try{ + var rewards = await Reward.find() + return rewards || []; + } + catch{ + return []; + } +}; + +exports.create = async(data) => { + try{ + var reward = new Reward(data); + return await reward.save(); + } + catch{ + return null; + } +}; + +exports.update = async(reward, data) => { + + try{ + if(!reward) return null; + + for (let i in data) { + reward[i] = data[i]; + reward.markModified(i); + } + + var updated = await reward.save(); + return updated; + } + catch{ + return null; + } +}; + +exports.remove = async(rewardId) => { + try{ + var result = await Reward.deleteOne({tid: rewardId}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; + +exports.removeAll = async() => { + try{ + var result = await Reward.deleteMany({}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } + }; diff --git a/rewards/rewards.routes.js b/rewards/rewards.routes.js new file mode 100644 index 0000000..ee53f72 --- /dev/null +++ b/rewards/rewards.routes.js @@ -0,0 +1,40 @@ +const RewardController = require('./rewards.controller'); +const AuthTool = require('../authorization/auth.tool'); +const config = require('../config'); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Higher permission, can read all users +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = function (app) { + + app.get('/rewards/:tid', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + RewardController.GetReward + ]); + + app.get('/rewards', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(SERVER), + RewardController.GetAll + ]); + + app.post('/rewards/add', [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + RewardController.AddReward + ]); + + app.delete("/rewards/:tid", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + RewardController.DeleteReward + ]); + + app.delete("/rewards", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + RewardController.DeleteAll + ]); +}; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..bf1b71d --- /dev/null +++ b/server.js @@ -0,0 +1,134 @@ +const fs = require('fs'); +const http = require('http'); +const https = require('https'); +const express = require('express'); +const config = require('./config.js'); +const Limiter = require('./tools/limiter.tool'); +const mongoose = require("mongoose"); + +const app = express(); + +// CONNECTION TO DATABASE +var user = ""; //User is optional if auth not enabled +if(config.mongo_user && config.mongo_pass) + user = config.mongo_user + ":" + config.mongo_pass + "@"; + +var connect = "mongodb://" + user + config.mongo_host + ":" + config.mongo_port + "/" + config.mongo_db + "?authSource=admin"; +mongoose.set('strictQuery', false); +mongoose.connection.on("connected", () => { + console.log("Connected to MongoDB!"); +}); +mongoose.connection.on('error', function(err) { + console.error('Connection to MongoDB failed!'); +}); +mongoose.connect(connect); + +//Limiter to prevent attacks +Limiter.limit(app); + +//Headers +app.use(function (req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Credentials', 'true'); + res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE'); + res.header('Access-Control-Expose-Headers', 'Content-Length'); + res.header('Access-Control-Allow-Headers', 'Accept, Authorization, Content-Type, X-Requested-With, Range'); + if (req.method === 'OPTIONS') { + return res.send(200); + } else { + return next(); + } +}); + +//Parse JSON body +app.use(express.json({ limit: "100kb" })); + +//Log request +app.use((req, res, next) => { + var today = new Date(); + var date = today.getFullYear() +'-'+(today.getMonth()+1)+'-'+today.getDate(); + var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); + var date_tag = "[" + date + " " + time + "]"; + console.log(date_tag + " " + req.method + " " + req.originalUrl); + next(); +}) + +//Route root DIR +app.get('/', function(req, res){ + res.status(200).send(config.api_title + " " + config.version); +}); + +//Public folder +app.use('/', express.static('public')) + +//Routing +const AuthorizationRouter = require('./authorization/auth.routes'); +AuthorizationRouter.route(app); + +const UsersRouter = require('./users/users.routes'); +UsersRouter.route(app); + +const CardsRouter = require('./cards/cards.routes'); +CardsRouter.route(app); + +const PacksRouter = require('./packs/packs.routes'); +PacksRouter.route(app); + +const DecksRouter = require('./decks/decks.routes'); +DecksRouter.route(app); + +const VariantRouter = require('./variants/variants.routes'); +VariantRouter.route(app); + +const MatchesRouter = require('./matches/matches.routes'); +MatchesRouter.route(app); + +const RewardsRouter = require('./rewards/rewards.routes'); +RewardsRouter.route(app); + +const MarketRouter = require('./market/market.routes'); +MarketRouter.route(app); + +const ActivityRouter = require("./activity/activity.routes"); +ActivityRouter.route(app); + +//Read SSL cert +var ReadSSL = function() +{ + var privateKey = fs.readFileSync(config.https_key, 'utf8'); + var certificate = fs.readFileSync(config.https_cert, 'utf8'); + var cert_authority = fs.readFileSync(config.https_ca, 'utf8'); + var credentials = {key: privateKey, cert: certificate, ca: cert_authority}; + return credentials; +}; + +//HTTP +if(config.allow_http){ + var httpServer = http.createServer(app); + httpServer.listen(config.port, function () { + console.log('http listening port %s', config.port); + }); +} + +//HTTPS +if(config.allow_https && fs.existsSync(config.https_key)) { + var httpsServer = https.createServer(ReadSSL(), app); + httpsServer.listen(config.port_https, function () { + console.log('https listening port %s', config.port_https); + }); + + //HTTPS auto-reload ssl + var timeout; + fs.watch(config.https_cert, () => { + clearTimeout(timeout); + timeout = setTimeout(() => { + httpsServer.setSecureContext(ReadSSL()); + }, 1000); + }); +} + +//Start jobs +const Jobs = require("./jobs/jobs"); +Jobs.InitJobs(); + +module.exports = app \ No newline at end of file diff --git a/tools/date.tool.js b/tools/date.tool.js new file mode 100644 index 0000000..4f87ace --- /dev/null +++ b/tools/date.tool.js @@ -0,0 +1,65 @@ +var DateTool = {}; + +// -------- Date & timestamp ------- +DateTool.isDate = function(date) +{ + if (Object.prototype.toString.call(date) === "[object Date]") { + return !isNaN(date.getTime()); + } + return false; +}; + +DateTool.tagToDate = function(tag) +{ + if(typeof tag !== "string") + return null; + + [year, month, day] = tag.split("-"); + var d = new Date(year, month - 1, day, 0, 0, 0, 0); + return DateTool.isDate(d) ? d : null; +}; + +DateTool.dateToTag = function(d) +{ + if(!DateTool.isDate(d)) + return ""; + + var year = '' + d.getFullYear(); + var month = '' + (d.getMonth() + 1); + var day = '' + d.getDate(); + if (day.length < 2) day = '0' + day; + return [year, month, day].join('-'); +}; + +DateTool.getStartOfDay = function(date){ + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +DateTool.addDays = function(date, days) { + return new Date(date.getTime() + days*24*60*60*1000); +} + +DateTool.addHours = function(date, hours) { + return new Date(date.getTime() + hours*60*60*1000); +} + +DateTool.addMinutes = function(date, minutes) { + return new Date(date.getTime() + minutes*60000); +} + +DateTool.minDate = function() +{ + return new Date(-8640000000000000); +} + +DateTool.maxDate = function() +{ + return new Date(8640000000000000); +} + +DateTool.countDays = function(from, to) { + const ms_per_day = 1000 * 60 * 60 * 24; + return Math.round((to - from) / ms_per_day); +} + +module.exports = DateTool; \ No newline at end of file diff --git a/tools/email.tool.js b/tools/email.tool.js new file mode 100644 index 0000000..1d35ea2 --- /dev/null +++ b/tools/email.tool.js @@ -0,0 +1,121 @@ + +const nodeMailer = require('nodemailer'); +const config = require('../config'); +const fs = require('fs'); +const path = require('path'); + +//Send One Mail +exports.SendEmail = function(email_to, subject, text, callback){ + + if(!config.smtp_enabled) + return; + + console.log("Sending email to: " + email_to); + + let transporter = nodeMailer.createTransport({ + host: config.smtp_server, + port: config.smtp_port, //Port must be 465 (encrypted) or 587 (STARTTSL, first pre-request is unencrypted to know the encryption method supported, followed by encrypted request) + secure: (config.smtp_port == "465"), //On port 587 secure must be false since it will first send unsecured pre-request to know which encryption to use + requireTLS: true, //Force using encryption on port 587 on the second request + auth: { + user: config.smtp_user, + pass: config.smtp_password, + } + }); + + let mailOptions = { + from: '"' + config.smtp_name + '" <' + config.smtp_email + '>', // sender address + to: email_to, // list of receivers + subject: subject, // Subject line + //text: text, // plain text body + html: text, // html body + }; + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + if(callback) + callback(false, error); + console.log(error); + return; + } + + if(callback) + callback(true); + }); + +}; + +//Send same mail to multiple recipients (emails array) +exports.SendEmailList = function(emails, subject, text, callback){ + + if(!config.smtp_enabled) + return; + + if(!Array.isArray(emails)) + return; + + if(emails.length == 0) + return; + + let transporter = nodeMailer.createTransport({ + pool: true, + host: config.smtp_server, + port: config.smtp_port, + secure: (config.smtp_port == "465"), + requireTLS: true, + auth: { + user: config.smtp_user, + pass: config.smtp_password, + } + }); + + var email_list = emails; + var email_from = '"' + config.smtp_name + '" <' + config.smtp_email + '>'; + var total = emails.length; + var sent_success = 0; + var sent_count = 0; + var ended = false; + + transporter.on("idle", function () { + + while (transporter.isIdle() && email_list.length > 0) + { + var email_to = email_list.shift(); + let mailOptions = { + from: email_from, + to: email_to, + subject: subject, + html: text, + }; + + transporter.sendMail(mailOptions, (error, info) => { + sent_count++; + if (!error) { + sent_success++; + } + + if(email_list.length == 0 && sent_count == total && !ended) + { + ended = true; + if(callback) + callback(sent_success); + } + }); + } + }); +}; + +exports.ReadTemplate = function(template) +{ + const rootDir = path.dirname(require.main.filename); + const fullpath = rootDir + "/emails/" + template; + + try{ + const html = fs.readFileSync(fullpath, "utf8"); + return html; + } + catch + { + return null; + } +} \ No newline at end of file diff --git a/tools/file.tool.js b/tools/file.tool.js new file mode 100644 index 0000000..cea9c99 --- /dev/null +++ b/tools/file.tool.js @@ -0,0 +1,16 @@ +var fs = require('fs'); + +exports.readFileArraySync = function(filename){ + + var data = fs.readFileSync(filename, {encoding: "utf8"}); + var adata = data.split('\r\n'); + return adata; +}; + +exports.readFileArray = function(filename, callback){ + + fs.readFile(filename, {encoding: "utf8"}, function(data){ + var adata = data.split('\r\n'); + callback(adata); + }); +}; diff --git a/tools/limiter.tool.js b/tools/limiter.tool.js new file mode 100644 index 0000000..403bed2 --- /dev/null +++ b/tools/limiter.tool.js @@ -0,0 +1,46 @@ +const RateLimit = require('express-rate-limit'); +//const Slowdown = require('express-slow-down'); +const config = require('../config.js'); + +exports.limit = function(app) +{ + //Restrict to access from domain only + app.use(function(req, res, next) + { + //Ip address + req.ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; + + if(config.ip_blacklist.includes(req.ip)) + return res.status(401).send("Forbidden"); + + //Check server host + var host = req.hostname; + if(config.api_url && host != config.api_url) + return res.status(401).send("Forbidden"); + + next(); + }); + + //Rate limiter + if(config.limiter_proxy) + app.enable('trust proxy'); // only if your server is behind a reverse proxy + + app.use(RateLimit({ + windowMs: config.limiter_window, + max: config.limiter_max, + skip: function(req) { return config.ip_whitelist.includes(req.ip); }, + })); + app.auth_limiter = RateLimit({ + windowMs: config.limiter_window, + max: config.limiter_auth_max, + skip: function(req) { return config.ip_whitelist.includes(req.ip); }, + handler: function (req, res) { + res.status(429).send({error: "Too many requests!"}); + }, + }); + app.post_limiter = RateLimit({ + windowMs: config.limiter_window, + max: config.limiter_post_max, + skip: function(req) { return config.ip_whitelist.includes(req.ip); }, + }); +} \ No newline at end of file diff --git a/tools/validator.tool.js b/tools/validator.tool.js new file mode 100644 index 0000000..1b9a220 --- /dev/null +++ b/tools/validator.tool.js @@ -0,0 +1,92 @@ +const FileTool = require('../tools/file.tool'); + +var Validator = {}; + +Validator.isInteger = function(value){ + return Number.isInteger(value); +} + +Validator.isNumber = function(value){ + return !isNaN(parseFloat(value)) && isFinite(value); +}; + +Validator.validateUsername = function(username){ + if(typeof username != "string") + return false; + + if(username.length < 3 || username.length > 50) + return false; + + //Cant have some special characters, must be letters or digits and start with a letter + var regex = /^[a-zA-Z][a-zA-Z\d]+$/; + if(!regex.test(username)) + return false; + + return true; +} + +Validator.validatePhone = function(phone){ + if(typeof phone != "string") + return false; + + if(phone.length < 7) + return false; + + if(!/^[0-9]+$/.test(phone)) + return false; + + return true; +} + +Validator.validateEmail = function(email){ + + if(typeof email != "string") + return false; + + if(email.length < 7 || email.length > 320) + return false; + + var regex_email = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + if(!regex_email.test(email)) + return false; + + return true; + +} + +Validator.validatePassword = function(pass){ + + if(typeof pass != "string") + return false; + + if(pass.length < 4 || pass.length > 50) + return false; + + //Password validations could be improved here + + return true; + +} + +Validator.countQuantity = function(array){ + + if (!array || !Array.isArray(array)) + return 0; + + var total = 0; + for (const elem of array) { + var q = elem.quantity || 1; + total += q; + } + + return total; +} + + +//Returns true or false checking if array has the expected quantity +Validator.validateArray = function(array, quantity){ + var nb = Validator.countQuantity(array); + return quantity == nb; +} + +module.exports = Validator; \ No newline at end of file diff --git a/tools/web.tool.js b/tools/web.tool.js new file mode 100644 index 0000000..684d058 --- /dev/null +++ b/tools/web.tool.js @@ -0,0 +1,100 @@ +var http = require('http'); +var url = require('url'); + +var WebTool = {}; + +// -------- Http ----------------- +WebTool.get = function(path, callback) { + + var hostname = url.parse(path).hostname; + var pathname = url.parse(path).pathname; + + var post_options = { + host: hostname, + port: '80', + path: pathname, + method: 'GET' + }; + + var request = http.request(post_options, function(res) { + res.setEncoding('utf8'); + var oData = ""; + res.on('data', function (chunk) { + oData += chunk; + }); + res.on('end', function(){ + callback(oData, res.statusCode); + }); + }); + + request.end(); +}; + +WebTool.post = function(path, data, callback) { + + var post_data = JSON.stringify(data); + var hostname = url.parse(path).hostname; + var pathname = url.parse(path).pathname; + + var post_options = { + host: hostname, + port: '80', + path: pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': post_data.length + } + }; + + var request = http.request(post_options, function(res) { + res.setEncoding('utf8'); + var oData = ""; + res.on('data', function (chunk) { + oData += chunk; + }); + res.on('end', function(){ + callback(oData, res.statusCode); + }); + }); + + request.write(post_data); + request.end(); +}; + +WebTool.toObject = function(json) +{ + try{ + var data = JSON.parse(json); + return data; + } + catch{ + return {}; + } +} + +WebTool.toJson = function(data) +{ + try{ + var data = JSON.stringify(json); + return data; + } + catch{ + return ""; + } +} + +WebTool.GenerateUID = function(length, numberOnly) +{ + var result = ''; + var characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + if(numberOnly) + characters = '0123456789'; + var charactersLength = characters.length; + for ( var i = 0; i < length; i++ ) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +module.exports = WebTool; \ No newline at end of file diff --git a/users/users.cards.controller.js b/users/users.cards.controller.js new file mode 100644 index 0000000..de21df9 --- /dev/null +++ b/users/users.cards.controller.js @@ -0,0 +1,529 @@ +const UserModel = require("./users.model"); +const PackModel = require("../packs/packs.model"); +const CardModel = require("../cards/cards.model"); +const VariantModel = require('../variants/variants.model'); +const UserTool = require("./users.tool"); +const CardTool = require("../cards/cards.tool"); +const Activity = require("../activity/activity.model"); +const config = require('../config'); + +exports.UpdateDeck = async(req, res) => { + + if(!req.params.deckId) + return res.status(400).send({error: "Invalid parameters"}); + + var userId = req.jwt.userId; + var deckId = req.params.deckId; + + var ndeck = { + tid: req.params.deckId, + title: req.body.title || "Deck", + hero: req.body.hero || {}, + cards: req.body.cards || [], + }; + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found: " + userId}); + + var decks = user.decks || []; + var found = false; + var index = 0; + for(var i=0; i 0) + decks.push(ndeck); + + //Delete deck + if(found && ndeck.cards.length == 0) + decks.splice(index, 1); + + var userData = { decks: decks}; + var upUser = await UserModel.update(user, userData); + if (!upUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + return res.status(200).send(upUser.decks); +}; + +exports.DeleteDeck = async(req, res) => { + + if(!req.params.deckId) + return res.status(400).send({error: "Invalid parameters"}); + + var userId = req.jwt.userId; + var deckId = req.params.deckId; + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found: " + userId}); + + var decks = user.decks || {}; + var index = -1; + for(var i=0; i= 0) + decks.splice(index, 1); + + var userData = { decks: decks}; + var upUser = await UserModel.update(user, userData); + if (!upUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + return res.status(200).send(upUser.decks); +}; + +exports.BuyCard = async (req, res) => { + + const userId = req.jwt.userId; + const cardId = req.body.card; + const variantId = req.body.variant; + const quantity = req.body.quantity || 1; + + if (!cardId || typeof cardId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!variantId || typeof variantId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Number.isInteger(quantity) || quantity <= 0) + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var card = await CardModel.get(cardId); + if (!card) + return res.status(404).send({ error: "Cant find card " + cardId }); + + if(card.cost <= 0) + return res.status(400).send({ error: "Can't be purchased" }); + + var variant = await VariantModel.get(variantId); + var factor = variant != null ? variant.cost_factor : 1; + var cost = quantity * factor * card.cost; + if(user.coins < cost) + return res.status(400).send({ error: "Not enough coins" }); + + user.coins -= cost; + + var valid = await UserTool.addCards(user, [{tid: cardId, variant: variantId, quantity: quantity}]); + if (!valid) + return res.status(500).send({ error: "Error when adding cards" }); + + //Update the user array + var updatedUser = await UserModel.save(user, ["coins", "cards"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {card: cardId, variant: variantId, quantity: quantity}; + const act = await Activity.LogActivity("user_buy_card", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + // ------------- + return res.status(200).send(); + +}; + +exports.SellCard = async (req, res) => { + + const userId = req.jwt.userId; + const cardId = req.body.card; + const variantId = req.body.variant; + const quantity = req.body.quantity || 1; + + if (!cardId || typeof cardId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!variantId || typeof variantId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Number.isInteger(quantity) || quantity <= 0) + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var card = await CardModel.get(cardId); + if (!card) + return res.status(404).send({ error: "Cant find card " + cardId }); + + if(card.cost <= 0) + return res.status(400).send({ error: "Can't be sold" }); + + var variant = await VariantModel.get(variantId); + + if(!UserTool.hasCard(user, cardId, variantId, quantity)) + return res.status(400).send({ error: "Not enough cards" }); + + var factor = variant != null ? variant.cost_factor : 1; + var cost = quantity * Math.round(card.cost * factor * config.sell_ratio); + user.coins += cost; + + var valid = await UserTool.addCards(user, [{tid: cardId, variant: variantId, quantity: -quantity}]); + if (!valid) + return res.status(500).send({ error: "Error when removing cards" }); + + //Update the user array + var updatedUser = await UserModel.save(user, ["coins", "cards"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {card: cardId, variant: variantId, quantity: quantity}; + const act = await Activity.LogActivity("user_sell_card", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + // ------------- + return res.status(200).send(); +}; + +exports.SellDuplicateCards = async (req, res) => { + + const userId = req.jwt.userId; + const rarityId = req.body.rarity || ""; //If not set, will sell cards of all rarities + const variantId = req.body.variant || ""; //If not set, will sell cards of all variants + const keep = req.body.keep; //Number of copies to keep + + if(typeof rarityId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(typeof variantId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Number.isInteger(keep) || keep < 0) + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var all_variants = await VariantModel.getAll(); + if (!all_variants) + return res.status(404).send({ error: "Cant find variants" }); + + var all_cards = await CardModel.getAll(); + if (!all_cards) + return res.status(404).send({ error: "Cant find cards" }); + + var cards_to_sell = []; + var coins = 0; + for(var i=0; i 0 && card.quantity > keep) + { + if(!variantId || card.variant == variantId) + { + if(!rarityId || card_data.rarity == rarityId) + { + var variant = UserTool.getData(all_variants, card.variant); + var quantity = card.quantity - keep; + var sell = {tid: card.tid, variant: card.variant, quantity: -quantity}; + var factor = variant != null ? variant.cost_factor : 1; + var cost = quantity * Math.round(card_data.cost * factor * config.sell_ratio); + cards_to_sell.push(sell); + coins += cost; + } + } + } + } + + if(cards_to_sell.length == 0) + return res.status(200).send(); + + user.coins += coins; + + var valid = await UserTool.addCards(user, cards_to_sell); + if (!valid) + return res.status(500).send({ error: "Error when removing cards" }); + + //Update the user array + var updatedUser = await UserModel.save(user, ["coins", "cards"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {rarity: rarityId, variant: variantId, keep: keep}; + const act = await Activity.LogActivity("user_sell_cards_duplicate", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + // ------------- + return res.status(200).send(); +}; + +exports.BuyPack = async (req, res) => { + + const userId = req.jwt.userId; + const packId = req.body.pack; + const quantity = req.body.quantity || 1; + + if (!packId || typeof packId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Number.isInteger(quantity) || quantity <= 0) + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var pack = await PackModel.get(packId); + if (!pack) + return res.status(404).send({ error: "Cant find pack " + packId }); + + if(pack.cost <= 0) + return res.status(400).send({ error: "Can't be purchased" }); + + var cost = quantity * pack.cost; + if(user.coins < cost) + return res.status(400).send({ error: "Not enough coins" }); + + user.coins -= cost; + + var valid = await UserTool.addPacks(user, [{tid: packId, quantity: quantity}]); + if (!valid) + return res.status(500).send({ error: "Error when adding packs" }); + + //Update the user array + var updatedUser = await UserModel.save(user, ["coins", "packs"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {pack: packId, quantity: quantity}; + const act = await Activity.LogActivity("user_buy_pack", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + // ------------- + return res.status(200).send(); + +}; + +exports.SellPack = async (req, res) => { + + const userId = req.jwt.userId; + const packId = req.body.pack; + const quantity = req.body.quantity || 1; + + if (!packId || typeof packId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if(!Number.isInteger(quantity) || quantity <= 0) + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var pack = await PackModel.get(packId); + if (!pack) + return res.status(404).send({ error: "Cant find pack " + packId }); + + if(pack.cost <= 0) + return res.status(400).send({ error: "Can't be sold" }); + + if(!UserTool.hasPack(user, packId, quantity)) + return res.status(400).send({ error: "Not enough coins" }); + + var cost = quantity * Math.round(pack.cost * config.sell_ratio); + user.coins += cost; + + var valid = await UserTool.addPacks(user, [{tid: packId, quantity: -quantity}]); + if (!valid) + return res.status(500).send({ error: "Error when adding packs" }); + + //Update the user array + var updatedUser = await UserModel.save(user, ["coins", "packs"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {pack: packId, quantity: quantity}; + const act = await Activity.LogActivity("user_sell_pack", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + // ------------- + return res.status(200).send(); + +}; + +exports.OpenPack = async (req, res) => { + + const userId = req.jwt.userId; + const packId = req.body.pack; + + if (!packId || typeof packId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var pack = await PackModel.get(packId); + if (!pack) + return res.status(404).send({ error: "Cant find pack " + packId }); + + if(!UserTool.hasPack(user, packId, 1)) + return res.status(400).send({ error: "You don't have this pack" }); + + var cardsToAdd = await CardTool.getPackCards(pack); + var validCards = await UserTool.addCards(user, cardsToAdd); + var validPacks = await UserTool.addPacks(user, [{tid: packId, quantity: -1}]); + + if (!validCards || !validPacks) + return res.status(500).send({ error: "Error when adding cards" }); + + //Update the user array + var updatedUser = await UserModel.save(user, ["cards", "packs"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {pack: packId, cards: cardsToAdd}; + const act = await Activity.LogActivity("user_open_pack", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + // ------------- + return res.status(200).send(cardsToAdd); + +}; + +exports.BuyAvatar = async (req, res) => { + + const userId = req.jwt.userId; + const avatarId = req.body.avatar; + + if (!avatarId || typeof avatarId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var cost = config.avatar_cost; + if(user.coins < cost) + return res.status(400).send({ error: "Not enough coins" }); + + if(UserTool.hasAvatar(user, avatarId)) + return res.status(400).send({ error: "Already have this avatar" }); + + user.coins -= cost; + UserTool.addAvatars(user, [avatarId]); + + //Update the user array + var updatedUser = await UserModel.save(user, ["coins", "avatars"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {avatar: avatarId}; + const act = await Activity.LogActivity("user_buy_avatar", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + return res.status(200).send(); +}; + +exports.BuyCardback = async (req, res) => { + + const userId = req.jwt.userId; + const cardbackId = req.body.cardback; + + if (!cardbackId || typeof cardbackId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var cost = config.cardback_cost; + if(user.coins < cost) + return res.status(400).send({ error: "Not enough coins" }); + + if(UserTool.hasCardback(user, cardbackId)) + return res.status(400).send({ error: "Already have this cardback" }); + + user.coins -= cost; + UserTool.addCardbacks(user, [cardbackId]); + + //Update the user array + var updatedUser = await UserModel.save(user, ["coins", "cardbacks"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + const activityData = {cardback: cardbackId}; + const act = await Activity.LogActivity("user_buy_cardback", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + return res.status(200).send(); +}; + +//Fix variant from previous version +exports.FixVariants = async (req, res) => +{ + var from = req.body.from || ""; + var to = req.body.to || ""; + + if (from && typeof packId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + if (to && typeof packId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + var users = await UserModel.getAll(); + var default_variant = await VariantModel.getDefault(); + var default_tid = default_variant ? default_variant.tid : ""; + var count = 0; + + for(var u=0; u { + + if(!req.body.email || !req.body.username || !req.body.password){ + return res.status(400).send({error: 'Invalid parameters'}); + } + + var email = req.body.email; + var username = req.body.username; + var password = req.body.password; + var avatar = req.body.avatar || ""; + + //Validations + if(!Validator.validateUsername(username)){ + return res.status(400).send({error: 'Invalid username'}); + } + + if(!Validator.validateEmail(email)){ + return res.status(400).send({error: 'Invalid email'}); + } + + if(!Validator.validatePassword(password)){ + return res.status(400).send({error: 'Invalid password'}); + } + + if(avatar && typeof avatar !== "string") + return res.status(400).send({error: "Invalid avatar"}); + + var user_username = await UserModel.getByUsername(username); + var user_email = await UserModel.getByEmail(email); + + if(user_username) + return res.status(400).send({error: 'Username already exists'}); + if(user_email) + return res.status(400).send({error: 'Email already exists'}); + + //Check if its first user + var nb_users = await UserModel.count(); + var permission = nb_users > 0 ? 1 : 10; //First user has 10 + var validation = nb_users > 0 ? 0 : 1; //First user has 1 + + //User Data + var user = {}; + + user.username = username; + user.email = email; + user.avatar = avatar; + user.permission_level = permission; + user.validation_level = validation; + + user.coins = config.start_coins; + user.elo = config.start_elo; + user.xp = 0; + + user.account_create_time = new Date(); + user.last_login_time = new Date(); + user.last_online_time = new Date(); + user.email_confirm_key = UserTool.generateID(20); + + UserTool.setUserPassword(user, password); + + //Create user + var nUser = await UserModel.create(user); + if(!nUser) + return res.status(500).send({ error: "Unable to create user" }); + + //Send confirm email + UserTool.sendEmailConfirmKey(nUser, user.email, user.email_confirm_key); + + // Activity Log ------------- + var activityData = {username: user.username, email: user.email }; + var act = await Activity.LogActivity("register", user.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + //Return response + return res.status(200).send({ success: true, id: nUser._id }); +}; + +exports.GetAll = async(req, res) => { + + let user_permission_level = parseInt(req.jwt.permission_level); + let is_admin = (user_permission_level >= config.permissions.SERVER); + + var list = await UserModel.getAll(); + for(var i=0; i { + var user = await UserModel.getById(req.params.userId); + if(!user) + user = await UserModel.getByUsername(req.params.userId); + + if(!user) + return res.status(404).send({error: "User not found " + req.params.userId}); + + let user_permission_level = parseInt(req.jwt.permission_level); + let is_admin = (user_permission_level >= config.permissions.SERVER); + if(is_admin || req.params.userId == req.jwt.userId || req.params.userId == req.jwt.username) + user = user.deleteSecrets(); + else + user = user.deleteAdminOnly(); + + user.server_time = new Date(); //Return server time + return res.status(200).send(user); +}; + +exports.EditUser = async(req, res) => { + + var userId = req.params.userId; + var avatar = req.body.avatar; + var cardback = req.body.cardback; + + if(!userId || typeof userId !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(avatar && typeof avatar !== "string") + return res.status(400).send({error: "Invalid avatar"}); + + if(cardback && typeof cardback !== "string") + return res.status(400).send({error: "Invalid avatar"}); + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found: " + userId}); + + var userData = {}; + + if(avatar && avatar.length < 50) + userData.avatar = avatar; + + if(cardback && cardback.length < 50) + userData.cardback = cardback; + + //Add other variables you'd like to be able to edit here + //Avoid allowing changing username, email or password here, since those require additional security validations and should have their own functions + + //Update user + var result = await UserModel.update(user, userData); + if(!result) + return res.status(400).send({error: "Error updating user: " + userId}); + + return res.status(200).send(result.deleteSecrets()); +}; + +exports.EditEmail = async(req, res) => { + + var userId = req.jwt.userId; + var email = req.body.email; + + if(!userId || typeof userId !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!email || !Validator.validateEmail(email)) + return res.status(400).send({error: "Invalid email"}); + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found: " + userId}); + + if(email == user.email) + return res.status(400).send({error: "Email unchanged"}); + + //Find email + var foundUserEmail = await UserModel.getByEmail(email); + if(foundUserEmail) + return res.status(403).send({error: "Email already exists"}); + + var prev_email = user.email; + var userData = {}; + userData.email = email; + userData.validation_level = 0; + userData.email_confirm_key = UserTool.generateID(20); + + //Update user + var result = await UserModel.update(user, userData); + if(!result) + return res.status(400).send({error: "Error updating user email: " + userId}); + + //Send confirmation email + UserTool.sendEmailConfirmKey(user, email, userData.email_confirm_key); + UserTool.sendEmailChangeEmail(user, prev_email, email); + + // Activity Log ------------- + var activityData = {prev_email: prev_email, new_email: email }; + var a = await Activity.LogActivity("edit_email", req.jwt.username, {activityData}); + if (!a) return res.status(500).send({ error: "Failed to log activity!!" }); + + return res.status(200).send(result.deleteSecrets()); +}; + +exports.EditPassword = async(req, res) => { + + var userId = req.jwt.userId; + var password = req.body.password_new; + var password_previous = req.body.password_previous; + + if(!userId || typeof userId !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!password || !password_previous || typeof password !== "string" || typeof password_previous !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found: " + userId}); + + let validPass = AuthTool.validatePassword(user, password_previous); + if(!validPass) + return res.status(401).send({error: "Invalid previous password"}); + + UserTool.setUserPassword(user, password); + + var result = await UserModel.save(user, ["password", "refresh_key", "password_recovery_key"]); + if(!result) + return res.status(500).send({error: "Error updating user password: " + userId}); + + //Send confirmation email + UserTool.sendEmailChangePassword(user, user.email); + + // Activity Log ------------- + var a = await Activity.LogActivity("edit_password", req.jwt.username, {}); + if (!a) return res.status(500).send({ error: "Failed to log activity!!" }); + + return res.status(204).send({}); +}; + +exports.EditPermissions = async(req, res) => { + + var userId = req.params.userId; + var permission_level = req.body.permission_level; + + if(!userId || typeof userId !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!Validator.isInteger(permission_level)) + return res.status(400).send({error: "Invalid permission"}); + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found: " + userId}); + + var userData = {}; + + //Change avatar + userData.permission_level = permission_level; + + //Update user + var result = await UserModel.update(user, userData); + if(!result) + return res.status(400).send({error: "Error updating user: " + userId}); + + // Activity Log ------------- + var activityData = {username: user.username, permission_level: userData.permission_level }; + var act = await Activity.LogActivity("edit_permission", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + return res.status(200).send(result.deleteSecrets()); +}; + +exports.ResetPassword = async(req, res) => { + + var email = req.body.email; + + if(!config.smtp_enabled) + return res.status(400).send({error: "Email SMTP is not configured"}); + + if(!email || typeof email !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + var user = await UserModel.getByEmail(email); + if(!user) + return res.status(404).send({error: "User not found: " + email}); + + user.password_recovery_key = UserTool.generateID(10, true); + await UserModel.save(user, ["password_recovery_key"]); + + UserTool.sendEmailPasswordRecovery(user, email); + + return res.status(204).send({}); +}; + + +exports.ResetPasswordConfirm = async(req, res) => { + + var email = req.body.email; + var code = req.body.code; + var password = req.body.password; + + if(!email || typeof email !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!code || typeof code !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!password || typeof password !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + var user = await UserModel.getByEmail(email); + if(!user) + return res.status(404).send({error: "User not found: " + email}); + + if(!user.password_recovery_key || user.password_recovery_key.toUpperCase() != code) + return res.status(403).send({error: "Invalid Recovery Code"}); + + UserTool.setUserPassword(user, password); + + var result = await UserModel.save(user, ["password", "refresh_key", "password_recovery_key"]); + if(!result) + return res.status(500).send({error: "Error updating user password: " + email}); + + return res.status(204).send({}); +}; + +//In this function all message are returned in direct text because the email link is accessed from browser +exports.ConfirmEmail = async (req, res) =>{ + + if(!req.params.userId || !req.params.code){ + return res.status(404).send("Code invalid"); + } + + var user = await UserModel.getById(req.params.userId); + if(!user) + return res.status(404).send("Code invalid"); + + if(user.email_confirm_key != req.params.code) + return res.status(404).send("Code invalid"); + + if(user.validation_level >= 1) + return res.status(400).send("Email already confirmed!"); + + //Code valid! + var data = {validation_level: Math.max(user.validation_level, 1)}; + await UserModel.update(user, data); + + return res.status(200).send("Email confirmed!"); +}; + +exports.ResendEmail = async(req, res) => +{ + var userId = req.jwt.userId; + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found " + userId}); + + if(user.validation_level > 0) + return res.status(403).send({error: "Email already confirmed"}); + + UserTool.sendEmailConfirmKey(user, user.email, user.email_confirm_key); + + return res.status(200).send(); +} + +exports.SendEmail = async (req, res) =>{ + + var subject = req.body.title; + var text = req.body.text; + var email = req.body.email; + + if(!subject || typeof subject !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!text || typeof text !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + if(!email || typeof email !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + Email.SendEmail(email, subject, text, function(result){ + console.log("Sent email to: " + email + ": " + result); + return res.status(200).send({success: result}); + }); +}; + +// reward is an object containing rewards to give +exports.GiveReward = async(req, res) => +{ + var userId = req.params.userId; + var reward = req.body.reward; + + //Validate params + if (!userId || typeof userId !== "string") + return res.status(400).send({ error: "Invalid parameters" }); + + if (!reward || typeof reward !== "object") + return res.status(400).send({ error: "Invalid parameters" }); + + //Get the user add update the array + var user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Cant find user " + userId }); + + var valid = await UserTool.GainUserReward(user, reward); + if (!valid) + return res.status(500).send({ error: "Error when adding rewards " + userId }); + + //Update the user array + var updatedUser = await UserModel.save(user, ["cards"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + // Activity Log ------------- + var activityData = {reward: reward, user: user.username}; + var act = await Activity.LogActivity("reward_give", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + // ------------- + return res.status(200).send(updatedUser.deleteSecrets()); +}; + +// reward is an ID of reward to give +exports.GainReward = async(req, res) => +{ + var userId = req.params.userId; + var rewardId = req.body.reward; + + if(!userId || !rewardId) + return res.status(400).send({error: "Invalid parameters"}); + + if(typeof rewardId !== "string") + return res.status(400).send({error: "Invalid parameters"}); + + var user = await UserModel.getById(userId); + if(!user) + return res.status(404).send({error: "User not found: " + userId}); + + var reward = await RewardModel.get(rewardId); + if(!reward) + return res.status(404).send({error: "Reward not found: " + rewardId}); + + if(reward.repeat && req.jwt.permission_level < config.permissions.SERVER) + return res.status(404).send({error: "Insufficient Permission"}); + + if(!reward.repeat && user.rewards.includes(rewardId)) + return res.status(403).send({error: "Reward already claimed: " + rewardId}); + + if(!reward.repeat && reward.group && user.rewards.includes(reward.group)) + return res.status(403).send({error: "Reward group already claimed: " + reward.group}); + + //Save reward + if(!user.rewards.includes(reward.tid)) + user.rewards.push(reward.tid); + + if(reward.group && !user.rewards.includes(reward.group)) + user.rewards.push(reward.group); + + //Add reward to user + var valid = await UserTool.GainUserReward(user, reward); + + //Check if succeed + if(!valid) + return res.status(500).send({error: "Failed adding reward: " + rewardId + " for " + userId}); + + //Update the user + var updatedUser = await UserModel.save(user, ["rewards", "xp", "coins", "cards", "decks", "avatars", "cardbacks"]); + if (!updatedUser) return res.status(500).send({ error: "Error updating user: " + userId }); + + //Log activity + var activityData = {reward: reward, user: user.username}; + var act = await Activity.LogActivity("reward_gain", req.jwt.username, activityData); + if (!act) return res.status(500).send({ error: "Failed to log activity!!" }); + + return res.status(200).send(user.deleteSecrets()); +}; + +exports.GetOnline = async(req, res) => +{ + //Count online users + var time = new Date(); + time = DateTool.addMinutes(time, -10); + + var count = 0; + var users = await UserModel.getAll(); + var usernames = []; + for(var i=0; i time) + { + usernames.push(user.username); + count++; + } + } + return res.status(200).send({online: count, total: users.length, users: usernames}); +}; + +exports.Delete = async(req, res) => { + UserModel.remove(req.params.userId); + return res.status(204).send({}); +}; \ No newline at end of file diff --git a/users/users.friends.controller.js b/users/users.friends.controller.js new file mode 100644 index 0000000..8f966b3 --- /dev/null +++ b/users/users.friends.controller.js @@ -0,0 +1,135 @@ +const UserModel = require("./users.model"); +const Activity = require("../activity/activity.model"); +const UserTool = require('./users.tool'); +const config = require('../config.js'); + +exports.AddFriend = async (req, res) => { + + const userId = req.jwt.userId; + const username = req.body.username; + + //Validate params + if (!username || !userId) { + return res.status(400).send({ error: "Invalid parameters" }); + } + + //Get the user + const user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Can't find user" }); + + const friend = await UserModel.getByUsername(username); + if (!friend) + return res.status(404).send({ error: "Can't find friend" }); + + if(user.id == friend.id) + return res.status(400).send({ error: "Can't add yourself" }); + + //Add Friend + if(!user.friends.includes(friend.username)) + user.friends.push(friend.username); + + //Add request other friend + if(!friend.friends.includes(user.username) && !friend.friends_requests.includes(user.username)) + friend.friends_requests.push(user.username) + + //Remove self request + if(user.friends_requests.includes(friend.username)) + user.friends_requests.remove(friend.username); + + //Update the user array + var updatedUser = await UserModel.save(user, ["friends", "friends_requests"]); + if (!updatedUser) return res.status(400).send({ error: "Error updating user" }); + + //Update the other user + var updatedFriend = await UserModel.save(friend, ["friends_requests"]); + if (!updatedFriend) return res.status(400).send({ error: "Error updating user" }); + + // ------------- + return res.status(200).send(updatedUser.deleteSecrets()); +}; + +exports.RemoveFriend = async(req, res) => { + + const userId = req.jwt.userId; + const username = req.body.username; + + //Validate params + if (!username || !userId) { + return res.status(400).send({ error: "Invalid parameters" }); + } + + //Get the user + const user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Can't find user" }); + + const friend = await UserModel.getByUsername(username); + if (!friend) + return res.status(404).send({ error: "Can't find friend" }); + + if(user.friends.includes(friend.username)) + user.friends.remove(friend.username); + if(user.friends_requests.includes(friend.username)) + user.friends_requests.remove(friend.username); + if(friend.friends_requests.includes(user.username)) + friend.friends_requests.remove(user.username) + + //Update the user array + var updatedUser = await UserModel.save(user, ["friends", "friends_requests"]); + if (!updatedUser) return res.status(400).send({ error: "Error updating user" }); + + var updatedFriend = await UserModel.save(friend, ["friends_requests"]); + if (!updatedFriend) return res.status(400).send({ error: "Error updating user" }); + + // ------------- + return res.status(200).send(updatedUser.deleteSecrets()); +}; + +exports.ListFriends = async(req, res) => +{ + const userId = req.jwt.userId; + + //Validate params + if (!userId) { + return res.status(400).send({ error: "Invalid parameters" }); + } + + //Get the user + const user = await UserModel.getById(userId); + if (!user) + return res.status(404).send({ error: "Can't find user" }); + + var friends_users = user.friends || []; + var requests_users = user.friends_requests || []; + + const friends = await UserModel.getUsernameList(friends_users); + if (!friends) + return res.status(404).send({ error: "Can't find user friends" }); + + const requests = await UserModel.getUsernameList(requests_users); + if (!requests) + return res.status(404).send({ error: "Can't find user friends" }); + + //Reduce visible fields + for(var i=0; i { + + try{ + var user = await User.findOne({_id: id}); + return user; + } + catch{ + return null; + } +}; + +exports.getByEmail = async(email) => { + + try{ + var regex = new RegExp(["^", email, "$"].join(""), "i"); + var user = await User.findOne({email: regex}); + return user; + } + catch{ + return null; + } +}; + +exports.getByUsername = async(username) => { + + try{ + var regex = new RegExp(["^", username, "$"].join(""), "i"); + var user = await User.findOne({username: regex}); + return user; + } + catch{ + return null; + } +}; + +exports.create = async(userData) => { + const user = new User(userData); + return await user.save(); +}; + +exports.getAll = async() => { + + try{ + var users = await User.find() + users = users || []; + return users; + } + catch{ + return []; + } +}; + +exports.getAllLimit = async(perPage, page) => { + + try{ + var users = await User.find().limit(perPage).skip(perPage * page) + users = users || []; + return users; + } + catch{ + return []; + } +}; + +//List users contained in the username list +exports.getUsernameList = async(username_list) => { + + try{ + var users = await User.find({ username: { $in: username_list } }); + return users || []; + } + catch{ + return []; + } +}; + +//Saves an already loaded User, by providing a string list of changed keys +exports.save = async(user, modified_list) => { + + try{ + if(!user) return null; + + if(modified_list) + { + for (let i=0; i { + + try{ + if(!user) return null; + + for (let i in userData) { + user[i] = userData[i]; + user.markModified(i); + } + + var updatedUser = await user.save(); + return updatedUser; + } + catch(e){ + console.log(e); + return null; + } +}; + +//Load, and then update a user, based on userId and an object containing new values +exports.patch = async(userId, userData) => { + + try{ + var user = await User.findById ({_id: userId}); + if(!user) return null; + + for (let i in userData) { + user[i] = userData[i]; + } + + var updatedUser = await user.save(); + return updatedUser; + } + catch(e){ + console.log(e); + return null; + } +}; + +exports.remove = async(userId) => { + + try{ + var result = await User.deleteOne({_id: userId}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; + +exports.count = async() => +{ + try{ + var count = await User.countDocuments({}); + return count; + } + catch{ + return 0; + } +} diff --git a/users/users.routes.js b/users/users.routes.js new file mode 100644 index 0000000..37bc0e6 --- /dev/null +++ b/users/users.routes.js @@ -0,0 +1,215 @@ +const UsersController = require('./users.controller'); +const UsersCardsController = require("./users.cards.controller"); +const UsersFriendsController = require("./users.friends.controller"); +const AuthTool = require('../authorization/auth.tool'); +const config = require('../config'); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Middle permission, can read all users and grant rewards +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = function (app) { + + //Body: username, email, password, avatar + app.post("/users/register", app.auth_limiter, [ + UsersController.RegisterUser, + ]); + + app.get("/users", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersController.GetAll, + ]); + + app.get("/users/:userId", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersController.GetUser, + ]); + + // USER - EDITS ---------------------- + + //Body: avatar, userId allows an admin to edit another user + app.post("/users/edit/:userId", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + AuthTool.isSameUserOr(ADMIN), + UsersController.EditUser, + ]); + + //Body: permission + app.post("/users/permission/edit/:userId", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + UsersController.EditPermissions, + ]); + + //Body: email + app.post("/users/email/edit", app.auth_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersController.EditEmail, + ]); + + //Body: password_previous, password_new + app.post("/users/password/edit", app.auth_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersController.EditPassword, + ]); + + //Body: email + app.post("/users/password/reset", app.auth_limiter, [ + UsersController.ResetPassword, + ]); + + //body: email, code, password (password is the new one) + app.post("/users/password/reset/confirm", app.auth_limiter, [ + UsersController.ResetPasswordConfirm, + ]); + + /*app.post("/users/delete/:userId", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + UsersController.Delete, + ]);*/ + + // USER - EMAIL CONFIRMATION --------------------------- + + //Email confirm + app.get("/users/email/confirm/:userId/:code", [ + UsersController.ConfirmEmail, + ]); + + //Ask to resend confirmation email + app.post("/users/email/resend", app.auth_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersController.ResendEmail, + ]); + + //Send a test email to one email address + //body: title, text, email + app.post("/users/email/send", app.auth_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + UsersController.SendEmail, + ]); + + // USER - CARDS -------------------------------------- + + app.post("/users/packs/open/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.OpenPack, + ]); + + app.post("/users/packs/buy/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.BuyPack, + ]); + + app.post("/users/packs/sell/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.SellPack, + ]); + + app.post("/users/cards/buy/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.BuyCard, + ]); + + app.post("/users/cards/sell/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.SellCard, + ]); + + app.post("/users/cards/sell/duplicate", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.SellDuplicateCards, + ]); + + app.post("/users/cards/variants/fix/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isSameUserOr(SERVER), + UsersCardsController.FixVariants, + ]); + + app.post("/users/avatar/buy", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.BuyAvatar, + ]); + + app.post("/users/cardback/buy", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.BuyCardback, + ]); + + + // USER - DECKS -------------------------------------- + + //Decks + app.post('/users/deck/:deckId', app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.UpdateDeck + ]); + app.delete('/users/deck/:deckId', app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersCardsController.DeleteDeck + ]); + + // USER - Friends -------------------------------------- + + //body: username (friend username) + app.post("/users/friends/add/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersFriendsController.AddFriend, + ]); + + //body: username (friend username) + app.post("/users/friends/remove/", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersFriendsController.RemoveFriend, + ]); + + app.get("/users/friends/list/", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + UsersFriendsController.ListFriends, + ]); + + // USER - REWARDS --------------------------- + + //body: reward (object containing all rewards to give, doesnt exist in mongo db) + app.post("/users/rewards/give/:userId", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(SERVER), + UsersController.GiveReward, + ]); + + //body: reward (ID of the reward to give already in mongo db), only SERVER can give repeating rewards + app.post("/users/rewards/gain/:userId", app.post_limiter, [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(USER), + AuthTool.isSameUserOr(SERVER), + UsersController.GainReward, + ]); + + // USER - STATS --------------------------- + + app.get("/online", [ + UsersController.GetOnline + ]); + +}; \ No newline at end of file diff --git a/users/users.tool.js b/users/users.tool.js new file mode 100644 index 0000000..6e7057c --- /dev/null +++ b/users/users.tool.js @@ -0,0 +1,352 @@ +const config = require('../config.js'); +const crypto = require('crypto'); +const Email = require('../tools/email.tool'); +const AuthTool = require('../authorization/auth.tool'); +const DeckModel = require('../decks/decks.model'); +const Validator = require('../tools/validator.tool'); +const VariantModel = require('../variants/variants.model.js'); + +const UserTool = {}; + +UserTool.generateID = function(length, easyRead) { + var result = ''; + var characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + if(easyRead) + characters = 'abcdefghijklmnpqrstuvwxyz123456789'; //Remove confusing characters like 0 and O + var charactersLength = characters.length; + for ( var i = 0; i < length; i++ ) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + } + +UserTool.setUserPassword = (user, password) => +{ + user.password = AuthTool.hashPassword(password); + user.password_recovery_key = ""; //After changing password, disable recovery until inited again + user.refresh_key = crypto.randomBytes(16).toString('base64'); //Logout previous logins by changing the refresh_key +} + +//--------- Rewards ----------- + +UserTool.GainUserReward = async(user, reward) => +{ + //Add reward to user + user.coins += reward.coins || 0; + user.xp += reward.xp || 0; + + UserTool.addAvatars(user, reward.avatars); + UserTool.addCardbacks(user, reward.cardbacks); + + //Add cards and decks + var valid_c = await UserTool.addCards(user, reward.cards || []); + var valid_p = await UserTool.addPacks(user, reward.packs || []); + var valid_d = await UserTool.addDecks(user, reward.decks || []); + return valid_c && valid_p && valid_d; +}; + +//--------- Cards, Packs and Decks -------- + +//newCards is just an array of string (card tid), or an array of object {tid: "", quantity: 1} +UserTool.addCards = async(user, newCards) => +{ + var cards = user.cards; + + if(!Array.isArray(cards) || !Array.isArray(newCards)) + return false; //Wrong params + + if(newCards.length == 0) + return true; //No card to add, succeeded + + //Count quantities + var prevTotal = Validator.countQuantity(cards); + var addTotal = Validator.countQuantity(newCards); + + var variant_default = await VariantModel.getDefault(); + var default_tid = variant_default ? variant_default.tid : ""; + + //Loop on cards to add + for (let c = 0; c < newCards.length; c++) { + + var cardAdd = newCards[c]; + var cardAddTid = typeof cardAdd === 'object' ? cardAdd.tid : cardAdd; + var cardAddVariant = typeof cardAdd === 'object' ? cardAdd.variant : default_tid; + var cardAddQ = typeof cardAdd === 'object' ? cardAdd.quantity : 1; + + if (cardAddTid && typeof cardAddTid === "string") { + var quantity = cardAddQ || 1; //default is 1 + var found = false; + + for (let i = 0; i < cards.length; i++) { + if (cards[i].tid == cardAddTid && cards[i].variant == cardAddVariant) { + cards[i].quantity += quantity; + found = true; + break; + } + } + + if (!found) { + cards.push({ + tid: cardAddTid, + variant: cardAddVariant, + quantity: quantity, + }); + } + } + } + + //Remove empty + for(var i=cards.length-1; i>=0; i--) + { + var card = cards[i]; + if(!card.quantity || card.quantity <= 0) + cards.splice(i, 1); + } + + //Validate quantities to make sure the array was updated correctly, this is to prevent users from loosing all their cards because of server error which would be terrible. + var valid = Validator.validateArray(cards, prevTotal + addTotal); + return valid; +}; + +UserTool.addPacks = async (user, newPacks) => { + + var packs = user.packs; + + if(!Array.isArray(packs) || !Array.isArray(newPacks)) + return false; //Wrong params + + if(newPacks.length == 0) + return true; //No pack to add, succeeded + + //Count quantities + var prevTotal = Validator.countQuantity(packs); + var addTotal = Validator.countQuantity(newPacks); + + //Loop on packs to add + for (let c = 0; c < newPacks.length; c++) { + + var packAdd = newPacks[c]; + var packAddTid = typeof packAdd === 'object' ? packAdd.tid : packAdd; + var packAddQ = typeof packAdd === 'object' ? packAdd.quantity : 1; + + if (packAddTid && typeof packAddTid === "string") { + var quantity = packAddQ || 1; //default is 1 + var found = false; + + for (let i = 0; i < packs.length; i++) { + if (packs[i].tid == packAddTid) { + packs[i].quantity += quantity; + found = true; + } + } + + if (!found) { + packs.push({ + tid: packAddTid, + quantity: quantity, + }); + } + } + } + + //Remove empty + for(var i=packs.length-1; i>=0; i--) + { + var pack = packs[i]; + if(!pack.quantity || pack.quantity <= 0) + packs.splice(i, 1); + } + + //Validate quantities to make sure the array was updated correctly, this is to prevent users from loosing all their packs because of server error which would be terrible. + var valid = Validator.validateArray(packs, prevTotal + addTotal); + return valid; +}; + +//newDecks is just an array of string (deck tid) +UserTool.addDecks = async(user, newDecks) => +{ + var decks = user.decks; + + if(!Array.isArray(decks) || !Array.isArray(newDecks)) + return false; //Wrong params + + if(newDecks.length == 0) + return true; //No deck to add, succeeded + + var ndecks = await DeckModel.getList(newDecks); + if(!ndecks) + return false; //Decks not found + + //Loop on cards to add + for (let c = 0; c < ndecks.length; c++) { + + var deckAdd = ndecks[c]; + var valid_c = await UserTool.addCards(user, deckAdd.cards); + if(!valid_c) + return false; //Failed adding cards + + decks.push({ + tid: deckAdd.tid + "_" + UserTool.generateID(5), + title: deckAdd.title || "", + hero: deckAdd.hero || {}, + cards: deckAdd.cards || [], + }); + } + + return true; +}; + +UserTool.addAvatars = (user, avatars) => +{ + if(!avatars || !Array.isArray(avatars)) + return; + + for (let i = 0; i < avatars.length; i++) { + var avatar = avatars[i]; + if(avatar && typeof avatar === "string" && !user.avatars.includes(avatar)) + user.avatars.push(avatar); + } +}; + +UserTool.addCardbacks = (user, cardbacks) => +{ + if(!cardbacks || !Array.isArray(cardbacks)) + return; + + for (let i = 0; i < cardbacks.length; i++) { + var cardback = cardbacks[i]; + if(cardback && typeof cardback === "string" && !user.cardbacks.includes(cardback)) + user.cardbacks.push(cardback); + } +}; + +UserTool.hasCard = (user, card_id, variant_id, quantity) => +{ + for (let c = 0; c < user.cards.length; c++) { + var acard = user.cards[c]; + var aquantity = acard.quantity || 1; + if(acard.tid == card_id && acard.variant == variant_id && aquantity >= quantity) + return true; + } + return false; +}; + +UserTool.hasPack = (user, card_tid, quantity) => +{ + for (let c = 0; c < user.packs.length; c++) { + var apack = user.packs[c]; + var aquantity = apack.quantity || 1; + if(apack.tid == card_tid && aquantity >= quantity) + return true; + } + return false; +}; + +UserTool.hasAvatar = (user, avatarId) => +{ + return user.avatars.includes(avatarId); +} + +UserTool.hasCardback = (user, cardbackId) => +{ + return user.cardbacks.includes(cardbackId); +} + +UserTool.getDeck = (user, deck_tid) => +{ + var deck = {}; + if(user && user.decks) + { + for(var i=0; i +{ + for(var i=0; i { + + if(!email || !user) return; + + var subject = config.api_title + " - Email Confirmation"; + var http = config.allow_https ? "https://" : "http://"; + var confirm_link = http + config.api_url + "/users/email/confirm/" + user.id + "/" + email_confirm_key; + + var text = "Hello " + user.username + "
"; + text += "Welcome!

"; + text += "To confirm your email, click here:
" + confirm_link + "

"; + text += "Thank you and see you soon!
"; + + Email.SendEmail(email, subject, text, function(result){ + console.log("Sent email to: " + email + ": " + result); + }); + +}; + +UserTool.sendEmailChangeEmail = (user, email, new_email) => { + + if(!email || !user) return; + + var subject = config.api_title + " - Email Changed"; + + var text = "Hello " + user.username + "
"; + text += "Your email was succesfully changed to: " + new_email + "
"; + text += "If you believe this is an error, please contact support immediately.

" + text += "Thank you and see you soon!
"; + + Email.SendEmail(email, subject, text, function(result){ + console.log("Sent email to: " + email + ": " + result); + }); +}; + +UserTool.sendEmailChangePassword = (user, email) => { + + if(!email || !user) return; + + var subject = config.api_title + " - Password Changed"; + + var text = "Hello " + user.username + "
"; + text += "Your password was succesfully changed
"; + text += "If you believe this is an error, please contact support immediately.

" + text += "Thank you and see you soon!
"; + + Email.SendEmail(email, subject, text, function(result){ + console.log("Sent email to: " + email + ": " + result); + }); + +}; + +UserTool.sendEmailPasswordRecovery = (user, email) => { + + if(!email || !user) return; + + var subject = config.api_title + " - Password Recovery"; + + var text = "Hello " + user.username + "
"; + text += "Here is your password recovery code: " + user.password_recovery_key.toUpperCase() + "

"; + text += "Thank you and see you soon!
"; + + Email.SendEmail(email, subject, text, function(result){ + console.log("Sent email to: " + email + ": " + result); + }); +}; + + +module.exports = UserTool; \ No newline at end of file diff --git a/variants/variants.controller.js b/variants/variants.controller.js new file mode 100644 index 0000000..e7e02c5 --- /dev/null +++ b/variants/variants.controller.js @@ -0,0 +1,66 @@ +const VariantModel = require("./variants.model"); + +exports.AddVariant = async(req, res) => +{ + var tid = req.body.tid; + var cost_factor = req.body.cost_factor || 1; + var is_default = req.body.is_default || false; + + if(!Number.isInteger(cost_factor)) + return res.status(400).send({ error: "Invalid parameters" }); + + var data = { + tid: tid, + cost_factor: cost_factor, + is_default: is_default, + } + + //Update or create + var variant = await VariantModel.get(tid); + if(variant) + variant = await VariantModel.update(variant, data); + else + variant = await VariantModel.create(data); + + if(!variant) + return res.status(500).send({error: "Error updating variant"}); + + return res.status(200).send(data); +}; + +exports.DeleteVariant = async(req, res) => +{ + VariantModel.remove(req.params.tid); + return res.status(204).send({}); +}; + +exports.DeleteAll = async(req, res) => +{ + VariantModel.removeAll(); + return res.status(204).send({}); +}; + +exports.GetVariant = async(req, res) => +{ + var tid = req.params.tid; + + if(!tid) + return res.status(400).send({error: "Invalid parameters"}); + + var variant = await VariantModel.get(tid); + if(!variant) + return res.status(404).send({error: "Variant not found: " + tid}); + + return res.status(200).send(variant.toObj()); +}; + +exports.GetAll = async(req, res) => +{ + var variants = await VariantModel.getAll(); + + for(var i=0; i { + + try{ + var variant = new Variant(data); + return await variant.save(); + } + catch{ + return null; + } +}; + +exports.get = async(variant_tid) => { + + try{ + var variant = await Variant.findOne({tid: variant_tid}); + return variant; + } + catch{ + return null; + } +}; + +exports.getDefault = async() => { + + try{ + var variant = await Variant.findOne({is_default: true}); + return variant; + } + catch{ + return null; + } +}; + +exports.getAll = async() => { + + try{ + var variants = await Variant.find({}); + return variants; + } + catch{ + return []; + } + +}; + +exports.update = async(variant, data) => { + + try{ + if(!variant) return null; + + for (let i in data) { + variant[i] = data[i]; + variant.markModified(i); + } + + var updated = await variant.save(); + return updated; + } + catch{ + return null; + } +}; + +exports.remove = async(variant_tid) => { + + try{ + var result = await Variant.deleteOne({tid: variant_tid}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; + +exports.removeAll = async() => { + try{ + var result = await Variant.deleteMany({}); + return result && result.deletedCount > 0; + } + catch{ + return false; + } +}; \ No newline at end of file diff --git a/variants/variants.routes.js b/variants/variants.routes.js new file mode 100644 index 0000000..07ff863 --- /dev/null +++ b/variants/variants.routes.js @@ -0,0 +1,36 @@ +const config = require("../config"); +const VariantsController = require("./variants.controller"); +const AuthTool = require("../authorization/auth.tool"); + +const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users +const SERVER = config.permissions.SERVER; //Higher permission, can read all users +const USER = config.permissions.USER; //Lowest permision, can only do things on same user + +exports.route = (app) => { + + app.get("/variants", [ + VariantsController.GetAll + ]); + + app.get("/variants/:tid", [ + VariantsController.GetVariant + ]); + + app.post("/variants/add", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + VariantsController.AddVariant + ]); + + app.delete("/variants/:tid", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + VariantsController.DeleteVariant + ]); + + app.delete("/variants", [ + AuthTool.isValidJWT, + AuthTool.isPermissionLevel(ADMIN), + VariantsController.DeleteAll + ]); +};