Compare commits
10 Commits
8d542ea201
...
02b47f7ecf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02b47f7ecf | ||
|
|
a730dc9974 | ||
|
|
03decff71d | ||
|
|
8c3ded3e20 | ||
|
|
ea390c6461 | ||
|
|
e26f405ea8 | ||
|
|
43803f024b | ||
|
|
8be2bf9dec | ||
|
|
6fa45b8f74 | ||
|
|
4b2bb35c20 |
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal 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
|
||||||
|
|
||||||
21
activity/activity.controller.js
Normal file
21
activity/activity.controller.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
||||||
56
activity/activity.model.js
Normal file
56
activity/activity.model.js
Normal 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 [];
|
||||||
|
}
|
||||||
|
};
|
||||||
19
activity/activity.routes.js
Normal file
19
activity/activity.routes.js
Normal 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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
170
authorization/auth.controller.js
Normal file
170
authorization/auth.controller.js
Normal 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();
|
||||||
|
}
|
||||||
48
authorization/auth.routes.js
Normal file
48
authorization/auth.routes.js
Normal 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
162
authorization/auth.tool.js
Normal 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
122
cards/cards.controller.js
Normal 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
111
cards/cards.model.js
Normal 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
42
cards/cards.routes.js
Normal 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
92
cards/cards.tool.js
Normal 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
77
config.js
Normal 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
75
decks/decks.controller.js
Normal 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
99
decks/decks.model.js
Normal 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
36
decks/decks.routes.js
Normal 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
39
jobs/jobs.js
Normal 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
211
ladder-api.md
Normal 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
562
ladder-config.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
55
ladder/ladder.controller.js
Normal file
55
ladder/ladder.controller.js
Normal 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
122
ladder/ladder.model.js
Normal 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
15
ladder/ladder.routes.js
Normal 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
173
ladder/ladder.service.js
Normal 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
215
market/market.controller.js
Normal 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
146
market/market.model.js
Normal 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
51
market/market.routes.js
Normal 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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
};
|
||||||
123
matches/matches.controller.js
Normal file
123
matches/matches.controller.js
Normal 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
87
matches/matches.model.js
Normal 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
34
matches/matches.routes.js
Normal 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
96
matches/matches.tool.js
Normal 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
16
package.json
Normal 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
80
packs/packs.controller.js
Normal 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
96
packs/packs.model.js
Normal 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
36
packs/packs.routes.js
Normal 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
|
||||||
|
]);
|
||||||
|
};
|
||||||
107
rewards/rewards.controller.js
Normal file
107
rewards/rewards.controller.js
Normal 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
109
rewards/rewards.model.js
Normal 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
40
rewards/rewards.routes.js
Normal 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
146
server.js
Normal 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
120
tasks-api.md
Normal 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
80
tasks-config.json
Normal 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
75
tasks/tasks.controller.js
Normal 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
96
tasks/tasks.model.js
Normal 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
30
tasks/tasks.routes.js
Normal 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
116
tasks/tasks.tool.js
Normal 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
65
tools/date.tool.js
Normal 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
121
tools/email.tool.js
Normal 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
16
tools/file.tool.js
Normal 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
46
tools/limiter.tool.js
Normal 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
92
tools/validator.tool.js
Normal 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
100
tools/web.tool.js
Normal 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;
|
||||||
545
users/users.cards.controller.js
Normal file
545
users/users.cards.controller.js
Normal 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
552
users/users.controller.js
Normal 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({});
|
||||||
|
};
|
||||||
135
users/users.friends.controller.js
Normal file
135
users/users.friends.controller.js
Normal 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
256
users/users.model.js
Normal 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
222
users/users.routes.js
Normal 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
354
users/users.tool.js
Normal 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;
|
||||||
66
variants/variants.controller.js
Normal file
66
variants/variants.controller.js
Normal 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
103
variants/variants.model.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
36
variants/variants.routes.js
Normal file
36
variants/variants.routes.js
Normal 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
|
||||||
|
]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user