Compare commits

...

10 Commits

Author SHA1 Message Date
chnxianyi
02b47f7ecf 更新 2025-09-30 17:39:59 +08:00
chnxianyi
a730dc9974 修改用户名&多任务奖励 2025-09-17 13:49:34 +08:00
chnxianyi
03decff71d 新增碎片&钻石 2025-09-11 18:00:55 +08:00
chnxianyi
8c3ded3e20 任务奖励 2025-09-11 13:51:35 +08:00
yaoyanwei
ea390c6461 1 2025-09-11 09:59:28 +08:00
yaoyanwei
e26f405ea8 1 2025-09-08 16:43:50 +08:00
yaoyanwei
43803f024b 优化 2025-08-26 15:01:46 +08:00
yaoyanwei
8be2bf9dec 增加天梯排行榜 2025-08-26 14:50:06 +08:00
yaoyanwei
6fa45b8f74 优化 2025-08-26 14:28:54 +08:00
yaoyanwei
4b2bb35c20 init 2025-08-04 16:25:38 +08:00
58 changed files with 6983 additions and 0 deletions

68
.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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);
};

View File

@@ -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 [];
}
};

View File

@@ -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,
]);
}

View File

@@ -0,0 +1,170 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const config = require('../config.js');
const jwtSecret = config.jwt_secret;
const Activity = require("../activity/activity.model");
const UserModel = require('../users/users.model');
const UserTool = require('../users/users.tool');
exports.Login = (req, res) => {
try {
let refresh_id = req.login.userId + jwtSecret;
let refresh_key = crypto.randomBytes(16).toString('base64');
let refresh_hash = crypto.createHmac('sha512', refresh_key).update(refresh_id).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 data = {
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(data);
} catch (err) {
return res.status(500).send({ error: err });
}
};
exports.SteamLogin = async (req, res) => {
try {
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 user = await UserModel.getByUsername(username);
if (!user) {
user = {};
user.username = username;
user.email = email;
user.permission_level = 1;
user.validation_level = 0;
user.coins = config.start_coins;
user.cardfragments = config.start_cardfragments;
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 user_created = await UserModel.create(user);
if (!user_created)
return res.status(500).send({ error: "Unable to create user" });
//Send confirm email
UserTool.sendEmailConfirmKey(user_created, user.email, user.email_confirm_key);
// Activity Log -------------
var act = await Activity.LogActivity("register", user.username, { username: user.username, email: user.email });
if (!act) return res.status(500).send({ error: "Failed to log activity!!" });
}
var login = {
userId: user.id,
username: user.username,
email: user.email,
permission_level: user.permission_level,
validation_level: user.validation_level,
provider: email ? 'email' : 'username',
}
let refresh_id = login.userId + jwtSecret;
let refresh_key = crypto.randomBytes(16).toString('base64');
let refresh_hash = crypto.createHmac('sha512', refresh_key).update(refresh_id).digest("base64");
login.refresh_key = refresh_key;
let access_token = jwt.sign(login, jwtSecret);
//Delete some keys for security, empty keys are never valid, also update login time
UserModel.patch(login.userId, { refresh_key: refresh_key, proof_key: "", password_recovery_key: "", last_login_time: new Date(), last_online_time: new Date() });
var data = {
id: login.userId,
username: login.username,
access_token: access_token,
refresh_token: refresh_hash,
permission_level: login.permission_level,
validation_level: login.validation_level,
duration: config.jwt_expiration,
server_time: new Date(),
version: config.version
}
return res.status(201).send(data);
} catch (err) {
console.error(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({description:config.description, version:config.version, md5:config.md5, timestamp: Date.now()});
};
// ----- 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();
}

View File

@@ -0,0 +1,48 @@
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.post('/auth/steam', app.auth_limiter, [
AuthController.SteamLogin
]);
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
]);
};

162
authorization/auth.tool.js Normal file
View File

@@ -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;

122
cards/cards.controller.js Normal file
View File

@@ -0,0 +1,122 @@
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 < cards.length; i++) {
var card = cards[i];
if (card && card.tid && card.type && card.team) {
var data = {
tid: card.tid,
type: card.type,
team: card.team,
rarity: card.rarity || "",
mana: card.mana || 0,
attack: card.attack || 0,
hp: card.hp || 0,
cost: card.cost || 0,
packs: card.packs || [],
};
var ccard = await CardModel.get(card.tid);
if (ccard) ccard = await CardModel.update(ccard, data);
else ccard = await CardModel.create(data);
ocards.push(ccard);
}
}
return res.status(200).send(ocards);
};
exports.DeleteCard = async (req, res) => {
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 < cards.length; i++) {
cards[i] = cards[i].toObj();
}
return res.status(200).send(cards);
};

111
cards/cards.model.js Normal file
View File

@@ -0,0 +1,111 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const cardsSchema = new Schema({
tid: { type: String, index: true, unique: true, default: "" },
type: { type: String, default: "" },
team: { type: String, default: "" },
rarity: {type: String, default: ""},
mana: {type: Number, default: 0},
attack: {type: Number, default: 0},
hp: {type: Number, default: 0},
cost: {type: Number, default: 0},
packs: [{type: String}], //Card is available in those packs
});
cardsSchema.methods.toObj = function() {
var card = this.toObject();
delete card.__v;
delete card._id;
return card;
};
const Card = mongoose.model('Cards', cardsSchema);
exports.get = async(tid) => {
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;
}
};

42
cards/cards.routes.js Normal file
View File

@@ -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
]);
};

92
cards/cards.tool.js Normal file
View File

@@ -0,0 +1,92 @@
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);
console.log("pack_cards", pack_cards);
var cards = [];
for (var i = 0; i < pack.cards; i++) {
if (pack.random) {
//Randomized set
var rarity_tid = CardTool.getRandomRarity(pack, i == 0);
var variant_tid = CardTool.getRandomVariant(pack);
var rarity_cards = CardTool.getCardArray(pack_cards, rarity_tid);
var card = CardTool.getRandomCard(rarity_cards);
if (card) {
var cardQ = { tid: card.tid, variant: variant_tid, quantity: 1 };
cards.push(cardQ);
}
} else if (i < pack_cards.length) {
//Fixed set
var card = pack_cards[i];
var variant_tid = CardTool.getRandomVariant(pack);
var cardQ = { tid: card.tid, variant: variant_tid, quantity: 1 };
cards.push(cardQ);
}
}
return cards;
};
CardTool.getRandomRarity = (pack, is_first) => {
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 < rarities.length; i++) {
var rarity = rarities[i];
if (rvalue < rarity.value) {
return rarity.tid;
}
rvalue -= rarity.value;
}
return "";
};
CardTool.getRandomVariant = (pack) => {
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 < variants.length; i++) {
var variant = variants[i];
if (rvalue < variant.value) {
return variant.tid;
}
rvalue -= variant.value;
}
return "";
};
CardTool.getCardArray = (all_cards, rarity) => {
var valid_cards = [];
for (var i = 0; i < all_cards.length; i++) {
var card = all_cards[i];
if (!rarity || card.rarity == rarity) valid_cards.push(card);
}
return valid_cards;
};
CardTool.getRandomCard = (all_cards, suffix) => {
if (all_cards.length > 0) {
var card = all_cards[Math.floor(Math.random() * all_cards.length)];
return card;
}
return null;
};
module.exports = CardTool;

77
config.js Normal file
View File

@@ -0,0 +1,77 @@
module.exports = {
version: "0.0.9",
description: "更新日志:0.0.9",
md5: "4be73c5ccf5bf5e956d172f7fd29d336",
port: 8080,
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: "mongodb",
mongo_pass: "WFSWiBkLPLZTzw7s",
mongo_host: "192.168.1.99",
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_cardfragments: 0,
start_crystals: 0,
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,
};

75
decks/decks.controller.js Normal file
View File

@@ -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);
};

99
decks/decks.model.js Normal file
View File

@@ -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;
}
};

36
decks/decks.routes.js Normal file
View File

@@ -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
]);
};

39
jobs/jobs.js Normal file
View File

@@ -0,0 +1,39 @@
const schedule = require('node-schedule');
const ladderModel = require('../ladder/ladder.model');
const ExecuteJobs = async() =>
{
//console.log('Run Hourly Jobs.....');
//Add custom hourly jobs here
};
// Execute leaderboard refresh at specified times: 00:00, 12:00, 18:00, 22:00
const RefreshLeaderboard = async() => {
console.log('Refreshing leaderboard...');
try {
await ladderModel.generateLeaderboard();
console.log('Leaderboard refreshed successfully');
} catch (error) {
console.error('Error refreshing leaderboard:', error);
}
};
exports.InitJobs = function()
{
// Hourly jobs
schedule.scheduleJob('* 1 * * *', function(){ // this for one hour
ExecuteJobs();
});
// Leaderboard refresh jobs at 00:00, 12:00, 18:00, 22:00
schedule.scheduleJob('0 0 * * *', async function() {
const hours = new Date().getHours();
if (hours === 0 || hours === 12 || hours === 18 || hours === 22) {
await RefreshLeaderboard();
}
});
// Test run when starting
ExecuteJobs();
}

211
ladder-api.md Normal file
View File

@@ -0,0 +1,211 @@
# 天梯系统API接口文档
## 概述
天梯系统API提供与玩家天梯等级、排行榜相关的接口。系统支持基于星星数的等级升降机制和王者分数机制。
## 接口列表
### 1. 获取排行榜
#### 接口地址
```
GET /ladder/leaderboard
```
#### 请求参数
#### 响应数据
```json
[
{
"playerId": "string", // 玩家ID
"username": "string", // 玩家用户名
"avatar": "string", // 玩家头像
"rankId": "number", // 天梯等级ID
"rankScore": "number", // 王者分数(如果适用)
"stars": "number", // 星星数(如果适用)
"totalWins": "number", // 总胜利场次
"position": "number" // 排名位置
}
]
```
#### 响应示例
```json
[
{
"playerId": "5f1a2b3c4d5e6f7g8h9i0j1k",
"username": "Player1",
"avatar": "avatar_01",
"rankId": 5,
"rankScore": 1250,
"stars": 0,
"totalWins": 128,
"position": 1
}
]
```
### 2. 获取玩家排名位置
#### 接口地址
```
GET /ladder/position/:playerId
```
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | ------ |
| playerId | string | 是 | 玩家ID |
#### 响应数据
```json
{
"position": "number" // 玩家在排行榜中的位置未上榜为null
}
```
#### 响应示例
```json
{
"position": 5
}
```
### 3. 获取玩家天梯信息
#### 接口地址
```
GET /ladder/rank/:playerId
```
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | ------ |
| playerId | string | 是 | 玩家ID |
#### 响应数据
对于开启王者分数机制的玩家:
```json
{
"rankName": "string", // 天梯阶级名称
"level": "number", // 天梯等级
"score": "number", // 王者分数
"isRankScore": "boolean" // 是否为王者分数机制
}
```
对于普通星星机制的玩家:
```json
{
"rankName": "string", // 天梯阶级名称
"level": "number", // 天梯等级
"stars": "number", // 当前星星数
"maxStars": "number", // 最大星星数
"isRankScore": "boolean" // 是否为王者分数机制
}
```
#### 响应示例
```json
{
"rankName": "Gold",
"level": 5,
"score": 1250,
"isRankScore": true
}
```
### 4. 获取所有天梯配置
#### 接口地址
```
GET /ladder/config
```
#### 请求参数
#### 响应数据
```json
[
{
"Id": "number", // 天梯ID
"Rank": "number", // 天梯阶级
"RankName": "string", // 天梯阶级名称
"Level": "number", // 天梯等级
"BeginStar": "number", // 升级后初始星星数
"RankDownStar": "number", // 降级后初始星星数
"MaxStar": "number", // 当前等级的星星上限
"WinGetStar": "number", // 胜利时增加的星星数
"ExtraGetStar": "number", // 连胜时额外增加的星星数
"LoseLostStar": "number", // 是否失败时减少星星1为是0为否
"LoseRankDown": "number", // 是否失败导致等级下降1为是0为否
"RankScore": "number", // 是否开启天梯分数机制1为开启
"AITimes": "number", // 等待多久后改为对战机器人
"AIDeck": "string", // 机器人卡组的RANK配置
"WaitTime": "number", // 扩大匹配范围的时间间隔
"MaxWaitTime": "number" // 王者等级匹配时,扩大分数范围的间隔
}
]
```
#### 响应示例
```json
[
{
"Id": 1,
"Rank": 1,
"RankName": "Bronze",
"Level": 1,
"BeginStar": 0,
"RankDownStar": 0,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 0,
"LoseLostStar": 1,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "bronze_ai",
"WaitTime": 10,
"MaxWaitTime": 20
}
]
```
## 错误响应格式
所有接口在出错时都会返回以下格式的错误信息:
```json
{
"error": "string" // 错误描述
}
```
## 天梯系统机制说明
### 星星机制
- 玩家通过胜利获得星星,失败可能失去星星
- 当星星数达到当前等级上限时,玩家升级
- 当星星数为0且再次失败时可能降级
### 王者分数机制
- 当玩家达到特定等级后开启王者分数机制
- 不再使用星星和等级升降,天梯等级固定
- 玩家首次进入该等级时获得1000分的初始天梯分数
- 胜利加分规则:
- 若对手分数 > 玩家分数:加分 = MIN(对手分数 / 玩家分数, 3) * 20
- 若玩家分数 >= 对手分数加20分
- 若对手未开启天梯分数加20分
- 失败减分规则:
- 若失败方已有天梯分数减20分最低为0分
- 若失败方未开启天梯分数根据配置决定是否扣除1颗星星
### 排行榜规则
- 展示天梯等级最高的前100名玩家
- 排名优先级:天梯等级 > 天梯分数 > 当前天梯星星数 > 玩家胜场数
- 每天的00:00、12:00、18:00、22:00四个时间点刷新排行榜

562
ladder-config.json Normal file
View File

@@ -0,0 +1,562 @@
[
{
"Id": 1,
"Rank": 1,
"RankName": "Bronze",
"Level": 1,
"BeginStar": 0,
"RankDownStar": 0,
"MaxStar": 2,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "bronze_ai",
"WaitTime": 10,
"MaxWaitTime": 20
},
{
"Id": 2,
"Rank": 1,
"RankName": "Bronze",
"Level": 2,
"BeginStar": 0,
"RankDownStar": 0,
"MaxStar": 2,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "bronze_ai",
"WaitTime": 10,
"MaxWaitTime": 20
},
{
"Id": 3,
"Rank": 1,
"RankName": "Bronze",
"Level": 3,
"BeginStar": 0,
"RankDownStar": 0,
"MaxStar": 2,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "bronze_ai",
"WaitTime": 10,
"MaxWaitTime": 20
},
{
"Id": 4,
"Rank": 1,
"RankName": "Bronze",
"Level": 4,
"BeginStar": 0,
"RankDownStar": 0,
"MaxStar": 2,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "bronze_ai",
"WaitTime": 10,
"MaxWaitTime": 20
},
{
"Id": 5,
"Rank": 1,
"RankName": "Bronze",
"Level": 5,
"BeginStar": 0,
"RankDownStar": 0,
"MaxStar": 2,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "bronze_ai",
"WaitTime": 10,
"MaxWaitTime": 20
},
{
"Id": 6,
"Rank": 2,
"RankName": "Silver",
"Level": 6,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 3,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "silver_ai",
"WaitTime": 15,
"MaxWaitTime": 25
},
{
"Id": 7,
"Rank": 2,
"RankName": "Silver",
"Level": 7,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 3,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "silver_ai",
"WaitTime": 15,
"MaxWaitTime": 25
},
{
"Id": 8,
"Rank": 2,
"RankName": "Silver",
"Level": 8,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 3,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "silver_ai",
"WaitTime": 15,
"MaxWaitTime": 25
},
{
"Id": 9,
"Rank": 2,
"RankName": "Silver",
"Level": 9,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 3,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "silver_ai",
"WaitTime": 15,
"MaxWaitTime": 25
},
{
"Id": 10,
"Rank": 2,
"RankName": "Silver",
"Level": 10,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 3,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 0,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "silver_ai",
"WaitTime": 15,
"MaxWaitTime": 25
},
{
"Id": 11,
"Rank": 3,
"RankName": "Gold",
"Level": 11,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 4,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "gold_ai",
"WaitTime": 20,
"MaxWaitTime": 30
},
{
"Id": 12,
"Rank": 3,
"RankName": "Gold",
"Level": 12,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 4,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "gold_ai",
"WaitTime": 20,
"MaxWaitTime": 30
},
{
"Id": 13,
"Rank": 3,
"RankName": "Gold",
"Level": 13,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 4,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "gold_ai",
"WaitTime": 20,
"MaxWaitTime": 30
},
{
"Id": 14,
"Rank": 3,
"RankName": "Gold",
"Level": 14,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 4,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "gold_ai",
"WaitTime": 20,
"MaxWaitTime": 30
},
{
"Id": 15,
"Rank": 3,
"RankName": "Gold",
"Level": 15,
"BeginStar": 0,
"RankDownStar": 2,
"MaxStar": 4,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 0,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "gold_ai",
"WaitTime": 20,
"MaxWaitTime": 30
},
{
"Id": 16,
"Rank": 4,
"RankName": "Platinum",
"Level": 16,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "platinum_ai",
"WaitTime": 25,
"MaxWaitTime": 35
},
{
"Id": 17,
"Rank": 4,
"RankName": "Platinum",
"Level": 17,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "platinum_ai",
"WaitTime": 25,
"MaxWaitTime": 35
},
{
"Id": 18,
"Rank": 4,
"RankName": "Platinum",
"Level": 18,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "platinum_ai",
"WaitTime": 25,
"MaxWaitTime": 35
},
{
"Id": 19,
"Rank": 4,
"RankName": "Platinum",
"Level": 19,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "platinum_ai",
"WaitTime": 25,
"MaxWaitTime": 35
},
{
"Id": 20,
"Rank": 4,
"RankName": "Platinum",
"Level": 20,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "platinum_ai",
"WaitTime": 25,
"MaxWaitTime": 35
},
{
"Id": 21,
"Rank": 5,
"RankName": "Diamond",
"Level": 21,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "diamond_ai",
"WaitTime": 30,
"MaxWaitTime": 40
},
{
"Id": 22,
"Rank": 5,
"RankName": "Diamond",
"Level": 22,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "diamond_ai",
"WaitTime": 30,
"MaxWaitTime": 40
},
{
"Id": 23,
"Rank": 5,
"RankName": "Diamond",
"Level": 23,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "diamond_ai",
"WaitTime": 30,
"MaxWaitTime": 40
},
{
"Id": 24,
"Rank": 5,
"RankName": "Diamond",
"Level": 24,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "diamond_ai",
"WaitTime": 30,
"MaxWaitTime": 40
},
{
"Id": 25,
"Rank": 5,
"RankName": "Diamond",
"Level": 25,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 0,
"AITimes": 30,
"AIDeck": "diamond_ai",
"WaitTime": 30,
"MaxWaitTime": 40
},
{
"Id": 26,
"Rank": 6,
"RankName": "King",
"Level": 26,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 1,
"InitialRankScore": 1100,
"WinScoreMin": 80,
"WinScoreMax": 150,
"LoseScore": 20,
"AITimes": 30,
"AIDeck": "king_ai",
"WaitTime": 35,
"MaxWaitTime": 45
},
{
"Id": 27,
"Rank": 6,
"RankName": "King",
"Level": 27,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 1,
"InitialRankScore": 1100,
"WinScoreMin": 80,
"WinScoreMax": 150,
"LoseScore": 20,
"AITimes": 30,
"AIDeck": "king_ai",
"WaitTime": 35,
"MaxWaitTime": 45
},
{
"Id": 28,
"Rank": 6,
"RankName": "King",
"Level": 28,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 1,
"InitialRankScore": 1100,
"WinScoreMin": 80,
"WinScoreMax": 150,
"LoseScore": 20,
"AITimes": 30,
"AIDeck": "king_ai",
"WaitTime": 35,
"MaxWaitTime": 45
},
{
"Id": 29,
"Rank": 6,
"RankName": "King",
"Level": 29,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 1,
"InitialRankScore": 1100,
"WinScoreMin": 80,
"WinScoreMax": 150,
"LoseScore": 20,
"AITimes": 30,
"AIDeck": "king_ai",
"WaitTime": 35,
"MaxWaitTime": 45
},
{
"Id": 30,
"Rank": 6,
"RankName": "King",
"Level": 30,
"BeginStar": 0,
"RankDownStar": 3,
"MaxStar": 5,
"WinGetStar": 1,
"ExtraGetStar": 1,
"LoseLostStar": 1,
"LoseRankDown": 1,
"RankScore": 1,
"InitialRankScore": 1100,
"WinScoreMin": 80,
"WinScoreMax": 150,
"LoseScore": 20,
"AITimes": 30,
"AIDeck": "king_ai",
"WaitTime": 35,
"MaxWaitTime": 45
}
]

View File

@@ -0,0 +1,55 @@
const ladderModel = require('./ladder.model');
const ladderService = require('./ladder.service');
const { UserModel } = require('../users/users.model');
// Get leaderboard
exports.getLeaderboard = async (req, res) => {
try {
const leaderboard = await ladderModel.getLeaderboard();
res.status(200).send(leaderboard);
} catch (error) {
console.error('Error getting leaderboard:', error);
res.status(500).send({ error: 'Failed to get leaderboard' });
}
};
// Get player's position in leaderboard
exports.getPlayerPosition = async (req, res) => {
try {
const { playerId } = req.params;
const position = await ladderModel.getPlayerPosition(playerId);
res.status(200).send({ position });
} catch (error) {
console.error('Error getting player position:', error);
res.status(500).send({ error: 'Failed to get player position' });
}
};
// Get player's rank info
exports.getPlayerRankInfo = async (req, res) => {
try {
const { playerId } = req.params;
const player = await UserModel.getById(playerId);
if (!player) {
return res.status(404).send({ error: 'Player not found' });
}
const rankInfo = ladderService.getPlayerRankInfo(player);
res.status(200).send(rankInfo);
} catch (error) {
console.error('Error getting player rank info:', error);
res.status(500).send({ error: 'Failed to get player rank info' });
}
};
// Get all rank configurations
exports.getRankConfigurations = async (req, res) => {
try {
const configs = ladderService.getAllRankConfigs();
res.status(200).send(configs);
} catch (error) {
console.error('Error getting rank configurations:', error);
res.status(500).send({ error: 'Failed to get rank configurations' });
}
};

122
ladder/ladder.model.js Normal file
View File

@@ -0,0 +1,122 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ladderService = require('./ladder.service');
const leaderboardSchema = new Schema({
playerId: { type: String, index: true, required: true },
username: { type: String, required: true },
avatar: { type: String, default: "" },
rankId: { type: Number, default: 1 },
rankScore: { type: Number, default: 0 },
stars: { type: Number, default: 0 },
totalWins: { type: Number, default: 0 },
position: { type: Number, required: true },
lastWinDeck: { type: Object, default: null }, // 添加最后获胜牌组信息
lastUpdated: { type: Date, default: Date.now }
});
const Leaderboard = mongoose.model('Leaderboard', leaderboardSchema);
// Leaderboard functions
exports.generateLeaderboard = async () => {
try {
// Clear current leaderboard
await Leaderboard.deleteMany({});
// Get all users
const User = mongoose.model('Users');
const users = await User.find({});
// Sort users based on ranking criteria:
// 1. Rank level (higher is better)
// 2. Rank score (for King rank mechanism)
// 3. Stars
// 4. Total wins
const sortedUsers = users.sort((a, b) => {
const configA = ladderService.getRankConfig(a.rankId);
const configB = ladderService.getRankConfig(b.rankId);
// Compare rank levels
if (configA.Level !== configB.Level) {
return configB.Level - configA.Level;
}
// For players with rank score mechanism (King rank)
if (configA.RankScore === 1 && configB.RankScore === 1) {
if (a.rankScore !== b.rankScore) {
return b.rankScore - a.rankScore;
}
}
// Compare stars
if (a.stars !== b.stars) {
return b.stars - a.stars;
}
// Compare total wins
return b.totalWins - a.totalWins;
});
// Take top 100 players
const topPlayers = sortedUsers.slice(0, 100);
// Create leaderboard entries
const leaderboardEntries = [];
for (let i = 0; i < topPlayers.length; i++) {
const player = topPlayers[i];
const entry = new Leaderboard({
playerId: player._id,
username: player.username,
avatar: player.avatar,
rankId: player.rankId,
rankScore: player.rankScore,
stars: player.stars,
totalWins: player.totalWins,
lastWinDeck: player.lastWinDeck, // 包含最后获胜牌组
position: i + 1
});
leaderboardEntries.push(entry);
}
// Save all entries
if (leaderboardEntries.length > 0) {
await Leaderboard.insertMany(leaderboardEntries);
}
return leaderboardEntries;
} catch (error) {
console.error('Error generating leaderboard:', error);
return [];
}
};
exports.getLeaderboard = async () => {
try {
const leaderboard = await Leaderboard.find({}).sort({ position: 1 });
return leaderboard;
} catch (error) {
console.error('Error getting leaderboard:', error);
return [];
}
};
exports.getPlayerPosition = async (playerId) => {
try {
const entry = await Leaderboard.findOne({ playerId: playerId });
return entry ? entry.position : null;
} catch (error) {
console.error('Error getting player position:', error);
return null;
}
};
// Initialize the ladder for a new player
exports.initializePlayerLadder = (player) => {
player.rankId = 1;
player.stars = 0;
player.rankScore = 0;
player.winStreak = 0;
player.totalWins = 0;
player.lastWinDeck = null; // 初始化最后获胜牌组
return player;
};

15
ladder/ladder.routes.js Normal file
View File

@@ -0,0 +1,15 @@
const ladderController = require('./ladder.controller');
exports.route = (app) => {
// Get leaderboard
app.get('/ladder/leaderboard', ladderController.getLeaderboard);
// Get player position in leaderboard
app.get('/ladder/position/:playerId', ladderController.getPlayerPosition);
// Get player rank info
app.get('/ladder/rank/:playerId', ladderController.getPlayerRankInfo);
// Get all rank configurations
app.get('/ladder/config', ladderController.getRankConfigurations);
};

173
ladder/ladder.service.js Normal file
View File

@@ -0,0 +1,173 @@
const fs = require('fs');
const path = require('path');
// Load ladder configuration
const ladderConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ladder-config.json')));
class LadderService {
constructor() {
this.rankConfigs = new Map();
this.initConfig();
}
initConfig() {
ladderConfig.forEach(config => {
this.rankConfigs.set(config.Id, config);
});
}
// Get rank configuration by ID
getRankConfig(rankId) {
return this.rankConfigs.get(rankId);
}
// Get all rank configurations
getAllRankConfigs() {
return Array.from(this.rankConfigs.values());
}
// Handle player win
async handleWin(player, opponent) {
const config = this.getRankConfig(player.rankId);
if (config.RankScore === 1) {
// King rank score mechanism
this.updateRankScore(player, opponent, true);
} else {
// Star-based mechanism
this.updateStars(player, config, true);
}
player.totalWins++;
player.winStreak++;
return player;
}
// Handle player loss
async handleLoss(player, opponent) {
const config = this.getRankConfig(player.rankId);
if (config.RankScore === 1) {
// King rank score mechanism
this.updateRankScore(player, opponent, false);
} else {
// Star-based mechanism
this.updateStars(player, config, false);
}
player.winStreak = 0;
return player;
}
updateStars(player, config, isWin) {
if (isWin) {
let starsToAdd = config.WinGetStar;
// Add extra stars for win streak (3+ wins)
if (player.winStreak >= 2) { // Streak starts at 2 (after 2 wins)
starsToAdd += config.ExtraGetStar;
}
player.stars += starsToAdd;
// Level up check
if (player.stars > config.MaxStar) {
this.levelUp(player);
}
} else {
if (config.LoseLostStar === 1) {
player.stars = Math.max(0, player.stars - 1);
// Level down check
if (player.stars === 0 && config.LoseRankDown === 1) {
this.levelDown(player, config);
}
}
}
}
updateRankScore(player, opponent, isWin) {
const config = this.getRankConfig(player.rankId);
if (isWin) {
// Initialize rank score if not already set
if (player.rankScore === 0) {
player.rankScore = config.InitialRankScore || 1100;
}
// Calculate score to add based on opponent's rank
let scoreToAdd = config.WinScoreMin || 80;
// If opponent has a higher rank score, calculate bonus
if (opponent.rankScore > player.rankScore) {
// Calculate bonus based on rank difference (simplified)
const ratio = Math.min((opponent.rankScore - player.rankScore) / 100, 1);
scoreToAdd = Math.round(config.WinScoreMin + ratio * (config.WinScoreMax - config.WinScoreMin));
}
player.rankScore += scoreToAdd;
} else {
// Deduct points on loss
const scoreToDeduct = config.LoseScore || 20;
if (player.rankScore > 0) {
player.rankScore = Math.max(0, player.rankScore - scoreToDeduct);
}
// Check for level down if score is too low
if (player.rankScore < 1000 && config.LoseRankDown === 1) {
this.levelDown(player, config);
}
}
}
levelUp(player) {
const nextConfig = this.getRankConfig(player.rankId + 1);
if (nextConfig) {
player.rankId++;
player.stars = nextConfig.BeginStar;
// Initialize rank score for King rank
if (nextConfig.RankScore === 1) {
player.rankScore = nextConfig.InitialRankScore || 1100;
}
}
}
levelDown(player, currentConfig) {
if (currentConfig.LoseRankDown === 1 && player.rankId > 1) {
player.rankId--;
const prevConfig = this.getRankConfig(player.rankId);
if (prevConfig.RankScore === 1) {
// Stay in rank score system
player.rankScore = Math.max(prevConfig.InitialRankScore || 1100, player.rankScore - (prevConfig.LoseScore || 20));
} else {
// Back to star system
player.stars = prevConfig.RankDownStar;
player.rankScore = 0;
}
}
}
// Get player's rank display info
getPlayerRankInfo(player) {
const config = this.getRankConfig(player.rankId);
if (config.RankScore === 1) {
return {
rankName: config.RankName,
level: config.Level,
score: player.rankScore,
isRankScore: true
};
} else {
return {
rankName: config.RankName,
level: config.Level,
stars: player.stars,
maxStars: config.MaxStar,
isRankScore: false
};
}
}
}
module.exports = new LadderService();

215
market/market.controller.js Normal file
View File

@@ -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<list.length; i++){
list[i] = list[i].toObj();
}
return res.status(200).send(list);
};
exports.getByCard = async(req, res) => {
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<list.length; i++){
list[i] = list[i].toObj();
}
return res.status(200).send(list);
};
exports.getOffer = async(req, res) => {
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<list.length; i++){
list[i] = list[i].toObj();
}
return res.status(200).send(list);
};

146
market/market.model.js Normal file
View File

@@ -0,0 +1,146 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const marketSchema = new Schema({
seller: {type: String, index: true},
card: {type: String, index: true},
variant: {type: String},
quantity: {type: Number},
price: {type: Number},
time: {type: Date},
});
marketSchema.methods.toObj = function() {
var offer = this.toObject();
delete offer.__v;
delete offer._id;
return offer;
};
const Market = mongoose.model('Markets', marketSchema);
// Market DATA MODELS ------------------------------------------------
exports.getOffer = async(user, card_tid, variant_id) => {
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;
}
};

51
market/market.routes.js Normal file
View File

@@ -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,
]);
};

View File

@@ -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);
};

87
matches/matches.model.js Normal file
View File

@@ -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;
}
};

34
matches/matches.routes.js Normal file
View File

@@ -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
]);
};

96
matches/matches.tool.js Normal file
View File

@@ -0,0 +1,96 @@
const UserTool = require('../users/users.tool');
const config = require('../config.js');
const ladderService = require('../ladder/ladder.service');
const ladderModel = require('../ladder/ladder.model');
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;
// Add ladder info to player data
data.rankId = player.rankId;
data.stars = player.stars;
data.rankScore = player.rankScore;
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;
// Handle ladder system
if (won) {
await ladderService.handleWin(player, opponent);
} else if (lost) {
await ladderService.handleLoss(player, opponent);
}
// Save last win deck for leaderboard
if (won && player.decks && player.decks.length > 0) {
// For simplicity, we'll use the first deck as the last win deck
// In a real implementation, this would be the actual deck used in the match
player.lastWinDeck = player.decks[0];
}
//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;
// Save player changes
await player.save();
var reward = {
elo: player.elo,
xp: xp,
coins: coins,
// Add ladder info to reward
rankId: player.rankId,
stars: player.stars,
rankScore: player.rankScore
};
return reward;
};
module.exports = MatchTool;

16
package.json Normal file
View File

@@ -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"
}
}

80
packs/packs.controller.js Normal file
View File

@@ -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<packs.length; i++){
packs[i] = packs[i].toObj();
}
return res.status(200).send(packs);
};

96
packs/packs.model.js Normal file
View File

@@ -0,0 +1,96 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const packsSchema = new Schema({
tid: { type: String, index: true, unique: true },
cards: {type: Number, default: 1}, //Number of cards per pack
cost: {type: Number, default: 0}, //Cost in coins
random: {type: Boolean, default: true},
rarities: [{type: Object}], //Probabilities to get each rarities
rarities_1st: [{type: Object}], //Probabilities but for the first card only
variants: [{type: Object}], //Probabilities of variants
});
packsSchema.methods.toObj = function() {
var elem = this.toObject();
delete elem.__v;
delete elem._id;
return elem;
};
const Pack = mongoose.model("Packs", packsSchema);
exports.Pack = Pack;
exports.create = async(data) => {
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;
}
};

36
packs/packs.routes.js Normal file
View File

@@ -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
]);
};

View File

@@ -0,0 +1,107 @@
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 cardfragments = req.body.cardfragments;
var crystals = req.body.crystals;
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(cardfragments && !Number.isInteger(cardfragments))
return res.status(400).send({error: "Invalid parameters"});
if(crystals && !Number.isInteger(crystals))
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,
cardfragments: cardfragments || 0,
crystals: crystals || 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);
};

109
rewards/rewards.model.js Normal file
View File

@@ -0,0 +1,109 @@
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 },
cardfragments: { type: Number, default: 0 },
crystals: { 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;
}
};

40
rewards/rewards.routes.js Normal file
View File

@@ -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
]);
};

146
server.js Normal file
View File

@@ -0,0 +1,146 @@
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: "10kb" }));
//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);
// Ladder system routes
const LadderRouter = require('./ladder/ladder.routes');
LadderRouter.route(app);
// Task system routes
const TasksRouter = require('./tasks/tasks.routes');
TasksRouter.route(app);
// Initialize task system
const TasksModel = require('./tasks/tasks.model.js');
TasksModel.initializeTaskConfig('./tasks-config.json');
//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

120
tasks-api.md Normal file
View File

@@ -0,0 +1,120 @@
# 任务系统接口文档
## 概述
任务系统允许玩家完成各种游戏内任务以获得奖励。系统支持多种任务类型,包括日常任务、成就任务等。
## API端点
### 获取所有任务配置
```
GET /api/tasks
```
**响应**
```json
[
{
"id": "login_task_1",
"name": "每日登录",
"desc": "每日登录游戏",
"condition": 1,
"value1": 1,
"value2": "",
"value3": "",
"rewardTypes": [0 , 1],
"rewardNums": [100 , 6],
"isDailyTask": true,
"durationHours": 24
},
{
"id": "win_task_1",
"name": "胜利之路",
"desc": "获得3场对战胜利",
"condition": 3,
"value1": 3,
"value2": "",
"value3": "",
"rewardTypes": [0],
"rewardNums": [200],
"isDailyTask": true,
"durationHours": 24
}
]
```
### 获取玩家任务数据
```
GET /api/tasks/{userId}
```
**响应**
```json
{
"tasks": [
{
"taskId": "login_task_1",
"assignedTime": 16409952000000000,
"expireTime": 16410816000000000,
"status": 1,
"progress": 1
}
],
"lastDailyTaskAssigned": 16409952000000000
}
```
### 保存玩家任务数据
```
POST /api/tasks/{userId}
```
**请求体**
```json
{
"tasks": [
{
"taskId": "login_task_1",
"assignedTime": 16409952000000000,
"expireTime": 16410816000000000,
"status": 1,
"progress": 1
}
],
"lastDailyTaskAssigned": 16409952000000000
}
```
**响应**
```json
{
"success": true,
"error": ""
}
```
## 枚举值定义
### 任务条件类型 (TaskConditionType)
| 值 | 名称 | 描述 |
|---|------|------|
| 1 | LoginGame | 登入游戏 |
| 2 | PlayGames | 进行X场对战 |
| 3 | WinGames | 胜利X场 |
| 4 | DefeatHeroWithAttributes | 击败Y属性和Z属性的英雄X次 |
| 5 | SummonHeroWithAttributes | 召唤Y属性和Z属性的英雄X次 |
| 6 | UseHeroSkillWithAttributes | 使用Y属性和Z属性英雄的技能X次 |
### 任务奖励类型 (TaskRewardType)
| 值 | 名称 | 描述 |
|---|------|------|
| 0 | Coins | 金币 |
| 1 | Crystal | 钻石 |
### 任务状态 (TaskStatus)
| 值 | 名称 | 描述 |
|---|------|------|
| 0 | Active | 激活 |
| 1 | Completed | 完成 |
| 2 | Expired | 过期 |
| 3 | Claimed | 已领取 |

80
tasks-config.json Normal file
View File

@@ -0,0 +1,80 @@
[
{
"id": "login_task_1",
"name": "每日登录",
"desc": "每日登录游戏",
"condition": 1,
"value1": 1,
"value2": "",
"value3": "",
"rewardTypes": [0 , 1],
"rewardNums": [100 , 6],
"isDailyTask": true,
"durationHours": 24
},
{
"id": "play_task_1",
"name": "参与对战",
"desc": "参与5场对战",
"condition": 2,
"value1": 5,
"value2": "",
"value3": "",
"rewardTypes": [0],
"rewardNums": [150],
"isDailyTask": true,
"durationHours": 24
},
{
"id": "win_task_1",
"name": "胜利之路",
"desc": "获得3场对战胜利",
"condition": 3,
"value1": 3,
"value2": "",
"value3": "",
"rewardTypes": [0],
"rewardNums": [200],
"isDailyTask": true,
"durationHours": 24
},
{
"id": "defeat_hero_task_1",
"name": "阵营克星",
"desc": "击败5个义勇军或帝国军英雄",
"condition": 4,
"value1": 5,
"value2": "YiYongJun",
"value3": "DiGuoJun",
"rewardTypes": [0],
"rewardNums": [300],
"isDailyTask": true,
"durationHours": 24
},
{
"id": "summon_hero_task_1",
"name": "召唤大师",
"desc": "召唤5个王国军或自由人英雄",
"condition": 5,
"value1": 5,
"value2": "WangGuoJun",
"value3": "ZiYouRen",
"rewardTypes": [0],
"rewardNums": [250],
"isDailyTask": true,
"durationHours": 24
},
{
"id": "skill_use_task_1",
"name": "技能专家",
"desc": "使用守郡或邪魔英雄的技能10次",
"condition": 6,
"value1": 10,
"value2": "ShouQun",
"value3": "XieMo",
"rewardTypes": [0],
"rewardNums": [350],
"isDailyTask": true,
"durationHours": 24
}
]

75
tasks/tasks.controller.js Normal file
View File

@@ -0,0 +1,75 @@
const TasksModel = require("./tasks.model.js");
const TasksTool = require("./tasks.tool.js");
// Get all task configurations
exports.getAllTasks = async (req, res) => {
try {
const tasks = await TasksModel.getAllTaskConfigs();
res.status(200).send(tasks);
} catch (error) {
res.status(500).send({ error: "Internal server error" });
}
};
// Get player tasks
exports.getPlayerTasks = async (req, res) => {
try {
const userId = req.params.userId;
if (!userId) {
return res.status(400).send({ error: "User ID is required" });
}
const playerTasks = await TasksModel.getPlayerTasks(userId);
if (!playerTasks) {
return res.status(404).send({ error: "Player tasks not found" });
}
res.status(200).send(playerTasks);
} catch (error) {
res.status(500).send({ error: "Internal server error" });
}
};
// Save player tasks
exports.savePlayerTasks = async (req, res) => {
try {
const userId = req.params.userId;
if (!userId) {
return res.status(400).send({ error: "User ID is required" });
}
const tasksData = req.body;
tasksData.userId = userId;
const historyTasks = await TasksModel.getPlayerTasks(userId);
const updatedTasks = await TasksModel.savePlayerTasks(tasksData);
if (!updatedTasks) {
return res.status(500).send({ error: "Failed to save player tasks" });
}
// Give reward for completing a task
for (let i = 0; i < req.body.tasks.length; i++) {
const task = req.body.tasks[i];
const historyTask = historyTasks.tasks.find(
(t) => t.taskId === task.taskId
);
if (historyTask) {
if (historyTask.status != task.status) {
if (
task.status === TasksTool.TASK_STATUS.CLAIMED &&
historyTask.status === TasksTool.TASK_STATUS.COMPLETED
) {
// 任务可以领取奖励 且 历史任务状态为完成
const taskConfig = await TasksModel.getTaskConfigById(task.taskId);
await TasksTool.giveTaskReward(userId, taskConfig);
}
}
}
}
res.status(200).send({ success: true });
} catch (error) {
res.status(500).send({ error: "Internal server error" });
}
};

96
tasks/tasks.model.js Normal file
View File

@@ -0,0 +1,96 @@
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
const Schema = mongoose.Schema;
// Task data structure based on the documentation
const playerTaskSchema = new Schema({
taskId: { type: String, required: true },
assignedTime: { type: Date, required: true },
expireTime: { type: Date, required: true },
status: { type: Number, required: true, default: 0 }, // 0: Active, 1: Completed, 2: Expired, 3: Claimed
progress: { type: Number, required: true, default: 0 },
});
const playerTaskDataSchema = new Schema({
userId: { type: String, required: true, index: true },
tasks: [playerTaskSchema],
lastDailyTaskAssigned: { type: Date, default: null },
});
playerTaskDataSchema.methods.toObj = function () {
var elem = this.toObject();
delete elem.__v;
delete elem._id;
return elem;
};
const PlayerTaskData = mongoose.model("PlayerTaskData", playerTaskDataSchema);
exports.PlayerTaskData = PlayerTaskData;
// Task configuration storage
let taskConfigurations = [];
// Get all task configurations
exports.getAllTaskConfigs = async () => {
return taskConfigurations;
};
// Get task configuration by ID
exports.getTaskConfigById = async (taskId) => {
return taskConfigurations.find((task) => task.id === taskId);
};
// Get player tasks
exports.getPlayerTasks = async (userId) => {
try {
let playerTaskData = await PlayerTaskData.findOne({ userId: userId });
if (!playerTaskData) {
playerTaskData = new PlayerTaskData({
userId: userId,
tasks: [],
lastDailyTaskAssigned: null,
});
await playerTaskData.save();
}
return playerTaskData;
} catch (e) {
console.error("Error getting player tasks:", e);
return null;
}
};
// Save player tasks
exports.savePlayerTasks = async (playerTaskData) => {
try {
const updated = await PlayerTaskData.findOneAndUpdate(
{ userId: playerTaskData.userId },
playerTaskData,
{ new: true, upsert: true }
);
return updated;
} catch (e) {
console.error("Error saving player tasks:", e);
return null;
}
};
// Initialize task configurations from file
exports.initializeTaskConfig = (configPath) => {
try {
if (fs.existsSync(configPath)) {
const configFile = fs.readFileSync(configPath, "utf8");
taskConfigurations = JSON.parse(configFile);
console.log(`Loaded ${taskConfigurations.length} task configurations`);
} else {
console.log(
"Task configuration file not found, using empty configuration"
);
taskConfigurations = [];
}
} catch (e) {
console.error("Error initializing task configuration:", e);
taskConfigurations = [];
}
};

30
tasks/tasks.routes.js Normal file
View File

@@ -0,0 +1,30 @@
const TasksController = require('./tasks.controller.js');
const AuthTool = require('../authorization/auth.tool.js');
const config = require('../config.js');
const ADMIN = config.permissions.ADMIN; // Highest permission, can read and write all users
const SERVER = config.permissions.SERVER; // Middle permission, can read all users
const USER = config.permissions.USER; // Lowest permission, can only do things on same user
exports.route = (app) => {
// Get all task configurations
app.get('/api/tasks',
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(USER),
TasksController.getAllTasks
);
// Get player tasks
app.get('/api/tasks/:userId',
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(USER),
TasksController.getPlayerTasks
);
// Save player tasks
app.post('/api/tasks/:userId',
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(USER),
TasksController.savePlayerTasks
);
};

116
tasks/tasks.tool.js Normal file
View File

@@ -0,0 +1,116 @@
const TasksModel = require("./tasks.model.js");
const UserModel = require("../users/users.model.js");
// Task status constants
const TASK_STATUS = {
ACTIVE: 0,
COMPLETED: 1,
EXPIRED: 2,
CLAIMED: 3
};
// Task condition types
const TASK_CONDITION_TYPES = {
LOGIN_GAME: 1,
PLAY_GAMES: 2,
WIN_GAMES: 3,
DEFEAT_HERO_WITH_ATTRIBUTES: 4,
SUMMON_HERO_WITH_ATTRIBUTES: 5,
USE_HERO_SKILL_WITH_ATTRIBUTES: 6
};
// Task reward types
const TASK_REWARD_TYPES = {
COINS: 0
};
// Check if a task is expired
exports.isTaskExpired = (task) => {
return new Date() > new Date(task.expireTime);
};
// Check if a task is completed
exports.isTaskCompleted = (task) => {
return task.status === TASK_STATUS.COMPLETED;
};
// Check if a task is claimed
exports.isTaskClaimed = (task) => {
return task.status === TASK_STATUS.CLAIMED;
};
// Get a random task from the configuration
exports.getRandomTask = async () => {
const tasks = await TasksModel.getAllTaskConfigs();
if (tasks.length === 0) {
return null;
}
const randomIndex = Math.floor(Math.random() * tasks.length);
return tasks[randomIndex];
};
// Create a new player task based on task configuration
exports.createPlayerTask = async (taskConfig) => {
const now = new Date();
const expireTime = new Date(now.getTime() + (taskConfig.durationHours * 60 * 60 * 1000));
return {
taskId: taskConfig.id,
assignedTime: now,
expireTime: expireTime,
status: TASK_STATUS.ACTIVE,
progress: 0
};
};
// Update task progress
exports.updateTaskProgress = async (playerTask, taskConfig, increment = 1) => {
// Check if task is already completed or claimed
if (playerTask.status === TASK_STATUS.COMPLETED || playerTask.status === TASK_STATUS.CLAIMED) {
return playerTask;
}
// Update progress
playerTask.progress += increment;
// Check if task is completed
if (playerTask.progress >= taskConfig.value1) {
playerTask.progress = taskConfig.value1; // Cap at the required value
playerTask.status = TASK_STATUS.COMPLETED;
}
return playerTask;
};
// Give reward for completing a task
exports.giveTaskReward = async (userId, taskConfig) => {
// In a real implementation, this would integrate with the rewards system
// For now, we'll just log the reward information
for (let i = 0; i < taskConfig.rewardTypes.length; i++) {
const rewardType = taskConfig.rewardTypes[i];
const rewardNum = taskConfig.rewardNums[i];
const user = await UserModel.getById(userId);
switch (rewardType) {
case TASK_REWARD_TYPES.COINS:
user.coins += rewardNum;
break;
case TASK_REWARD_TYPES.CRYSTALS:
user.crystals += rewardNum;
break;
case TASK_REWARD_TYPES.CARD_FRAGMENTS:
user.cardfragments += rewardNum;
break;
default:
console.log(`Unknown reward type: ${rewardType}`);
}
}
await user.save();
return true;
};
// Export constants
exports.TASK_STATUS = TASK_STATUS;
exports.TASK_CONDITION_TYPES = TASK_CONDITION_TYPES;
exports.TASK_REWARD_TYPES = TASK_REWARD_TYPES;

65
tools/date.tool.js Normal file
View File

@@ -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;

121
tools/email.tool.js Normal file
View File

@@ -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;
}
}

16
tools/file.tool.js Normal file
View File

@@ -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);
});
};

46
tools/limiter.tool.js Normal file
View File

@@ -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); },
});
}

92
tools/validator.tool.js Normal file
View File

@@ -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-Z0-9_]+$/;
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;

100
tools/web.tool.js Normal file
View File

@@ -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;

View File

@@ -0,0 +1,545 @@
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",
cover: req.body.cover || "",
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 < decks.length; i++) {
var deck = decks[i];
if (deck.tid == deckId) {
decks[i] = ndeck;
found = true;
index = i;
}
}
//Add new
if (!found && ndeck.cards.length > 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 < decks.length; i++) {
var deck = decks[i];
if (deck.tid == deckId) {
index = i;
}
}
if (index >= 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.cardfragments < cost)
return res.status(400).send({ error: "Not enough cardfragments" });
user.cardfragments -= 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, ["cardfragments", "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.cardfragments += 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, ["cardfragments", "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 cardfragments = 0;
for (var i = 0; i < user.cards.length; i++) {
var card = user.cards[i];
var card_data = UserTool.getData(all_cards, card.tid);
if (card_data && card_data.cost > 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);
cardfragments += cost;
}
}
}
}
if (cards_to_sell.length == 0) return res.status(200).send();
user.cardfragments += cardfragments;
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, ["cardfragments", "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();
};
// 购买卡包 TODO 金币&钻石
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" });
console.log("pack", pack);
var cardsToAdd = await CardTool.getPackCards(pack);
var validCards = await UserTool.addCards(user, cardsToAdd);
var validPacks = await UserTool.addPacks(user, [
{ tid: packId, quantity: -1 },
]);
console.log("getPackCards", cardsToAdd);
console.log("validPacks", validPacks);
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 < users.length; u++) {
var user = users[u];
var changed = false;
for (var i = 0; i < user.cards.length; i++) {
var card = user.cards[i];
if (!card.variant) {
card.variant = default_tid;
changed = true;
}
if (from && to && card.variant == from) {
card.variant = to;
changed = true;
}
}
if (changed) {
var new_cards = user.cards;
user.cards = [];
await UserTool.addCards(user, new_cards); //Re-add in correct format
UserModel.save(user, ["cards"]);
count++;
}
}
// Activity Log -------------
const act = await Activity.LogActivity("fix_variants", req.jwt.username, {});
if (!act) return res.status(500).send({ error: "Failed to log activity!!" });
return res.status(200).send({ updated: count });
};

552
users/users.controller.js Normal file
View File

@@ -0,0 +1,552 @@
const UserModel = require('./users.model');
const UserTool = require('./users.tool');
const RewardModel = require('../rewards/rewards.model');
const DateTool = require('../tools/date.tool');
const Activity = require("../activity/activity.model");
const Validator = require('../tools/validator.tool');
const AuthTool = require('../authorization/auth.tool');
const Email = require('../tools/email.tool');
const config = require('../config');
const ladderModel = require('../ladder/ladder.model');
//Register new user
exports.RegisterUser = async (req, res, next) => {
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.cardfragments = config.start_cardfragments;
user.crystals = config.start_crystals;
user.elo = config.start_elo;
user.xp = 0;
// Initialize ladder data
user = ladderModel.initializePlayerLadder(user);
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<list.length; i++){
if(is_admin)
list[i] = list[i].deleteSecrets();
else
list[i] = list[i].deleteAdminOnly();
}
return res.status(200).send(list);
};
exports.GetUser = async(req, res) => {
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.EditUsername = async(req, res) => {
var userId = req.jwt.userId;
var username = req.body.username;
if(!userId || typeof userId !== "string")
return res.status(400).send({error: "Invalid parameters"});
if(!username || !Validator.validateUsername(username))
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(username == user.username)
return res.status(400).send({error: "Username unchanged"});
//Find username
var foundUserUsername = await UserModel.getByUsername(username);
if(foundUserUsername)
return res.status(403).send({error: "Username already exists"});
var prev_username = user.username;
var userData = {};
userData.username = username;
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 username: " + userId});
// Activity Log -------------
var activityData = {prev_username: prev_username, new_username: username };
var a = await Activity.LogActivity("edit_username", 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", "cardfragments", "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<users.length; i++)
{
var user = users[i];
if(user.last_online_time > 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({});
};

View File

@@ -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<friends.length; i++)
{
friends[i] = {
username: friends[i].username,
avatar: friends[i].avatar,
last_online_time: friends[i].last_online_time,
}
}
for(var i=0; i<requests.length; i++)
{
requests[i] = {
username: requests[i].username,
avatar: requests[i].avatar,
last_online_time: requests[i].last_online_time,
}
}
return res.status(200).send({username: user.username, friends: friends, friends_requests: requests, server_time: new Date()});
}

256
users/users.model.js Normal file
View File

@@ -0,0 +1,256 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
username: {type: String, required: true, index: true, unique: true, default: ""},
email: {type: String, required: true, index: true, default: ""},
password: {type: String, required: true, default: ""},
permission_level: {type: Number, required: true, default: 1}, //Admin or not?
validation_level: {type: Number, required: true, default: 0}, //Validation level increases by confirming email
account_create_time: {type: Date, default: null},
last_login_time: {type: Date, default: null},
last_online_time: {type: Date, default: null},
refresh_key: {type: String, default: ""}, //Used for refreshing the current login JWT token
proof_key: {type: String, default: ""}, //Used to proof to a another server who you are
email_confirm_key: {type: String, default: ""}, //Used to confirm email
password_recovery_key: {type: String, default: ""}, //Used for password recovery
avatar: {type: String, default: ""},
cardback: {type: String, default: ""},
coins: {type: Number, default: 0},
cardfragments: {type: Number, default: 0},
crystals: {type: Number, default: 0},
xp: {type: Number, default: 0},
elo: {type: Number, default: 1000},
matches: {type: Number, default: 0},
victories: {type: Number, default: 0},
defeats: {type: Number, default: 0},
// Ladder system fields
rankId: {type: Number, default: 1}, // Current rank ID
stars: {type: Number, default: 0}, // Current stars
rankScore: {type: Number, default: 0}, // Rank score for王者分数 mechanism
winStreak: {type: Number, default: 0}, // Win streak counter
totalWins: {type: Number, default: 0}, // Total wins for leaderboard
lastWinDeck: {type: Object, default: null}, // Last winning deck for leaderboard
cards: [{ tid: String, variant: String, quantity: Number, _id: false }],
packs: [{ tid: String, quantity: Number, _id: false }],
decks: [{ type: Object, _id: false }],
avatars: [{type: String}],
cardbacks: [{type: String}],
rewards: [{type: String}],
friends: [{type: String}],
friends_requests: [{type: String}],
});
userSchema.virtual('id').get(function () {
return this._id.toHexString();
});
userSchema.methods.toObj = function() {
var user = this.toObject();
user.id = user._id;
delete user.__v;
delete user._id;
return user;
};
//Hide sensitive information
userSchema.methods.deleteSecrets = function(){
var user = this.toObject();
user.id = user._id;
delete user.__v;
delete user._id;
delete user.password;
delete user.refresh_key;
delete user.proof_key;
delete user.email_confirm_key;
delete user.password_recovery_key;
return user;
};
//Hide non-admin information, for example only admins can read user emails
userSchema.methods.deleteAdminOnly = function(){
var user = this.toObject();
delete user.__v;
delete user._id;
delete user.email;
delete user.permission_level;
delete user.validation_level;
delete user.password;
delete user.refresh_key;
delete user.proof_key;
delete user.email_confirm_key;
delete user.password_recovery_key;
return user;
};
const User = mongoose.model('Users', userSchema);
exports.UserModel = User;
// USER DATA MODELS ------------------------------------------------
exports.getById = async(id) => {
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<modified_list.length; i++) {
user.markModified(modified_list[i]);
}
}
return await user.save();
}
catch(e){
console.log(e);
return null;
}
};
//Update an already loaded user, by providing an object containing new values
exports.update = async(user, userData) => {
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;
}
}

222
users/users.routes.js Normal file
View File

@@ -0,0 +1,222 @@
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: username
app.post("/users/username/edit", app.auth_limiter, [
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(USER),
UsersController.EditUsername,
]);
//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
]);
};

354
users/users.tool.js Normal file
View File

@@ -0,0 +1,354 @@
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.cardfragments += reward.cardfragments || 0;
user.crystals += reward.crystals || 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<user.decks.length; i++)
{
var adeck = user.decks[i];
if(adeck.tid == deck_tid)
{
deck = adeck;
}
}
}
return deck;
};
UserTool.getData = (all_data, tid) =>
{
for(var i=0; i<all_data.length; i++)
{
if(all_data[i].tid == tid)
return all_data[i];
}
return null;
};
//--------- Emails --------
UserTool.sendEmailConfirmKey = (user, email, email_confirm_key) => {
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 + "<br>";
text += "Welcome! <br><br>";
text += "To confirm your email, click here: <br><a href='" + confirm_link + "'>" + confirm_link + "</a><br><br>";
text += "Thank you and see you soon!<br>";
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 + "<br>";
text += "Your email was succesfully changed to: " + new_email + "<br>";
text += "If you believe this is an error, please contact support immediately.<br><br>"
text += "Thank you and see you soon!<br>";
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 + "<br>";
text += "Your password was succesfully changed<br>";
text += "If you believe this is an error, please contact support immediately.<br><br>"
text += "Thank you and see you soon!<br>";
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 + "<br>";
text += "Here is your password recovery code: " + user.password_recovery_key.toUpperCase() + "<br><br>";
text += "Thank you and see you soon!<br>";
Email.SendEmail(email, subject, text, function(result){
console.log("Sent email to: " + email + ": " + result);
});
};
module.exports = UserTool;

View File

@@ -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<variants.length; i++){
variants[i] = variants[i].toObj();
}
return res.status(200).send(variants);
};

103
variants/variants.model.js Normal file
View File

@@ -0,0 +1,103 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const variantsSchema = new Schema({
tid: { type: String, index: true, unique: true },
cost_factor: {type: Number, default: 1}, //Cost multiplier
is_default: {type: Boolean, default: false},
});
variantsSchema.methods.toObj = function() {
var elem = this.toObject();
delete elem.__v;
delete elem._id;
return elem;
};
const Variant = mongoose.model("Variants", variantsSchema);
exports.Variant = Variant;
exports.create = async(data) => {
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;
}
};

View File

@@ -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
]);
};