*Added onlineCount endpoint
*Added launch with PM2 *Code clenaup
This commit is contained in:
parent
5dfa4d83e7
commit
b400beea6a
18 changed files with 381 additions and 321 deletions
|
|
@ -111,19 +111,20 @@ The api provides the following endpoints:
|
|||
|
||||
Endpoint | Method | Arguments | Description
|
||||
--- | --- | --- | ---
|
||||
/serverApi/auth | POST | XML with account, password, game and IP | Authenticates a user based on their account information and sends an XML response with their user ID, user type, and success status. If authentication fails, it sends an XML response with a failure status.
|
||||
/serverApi/auth | POST | XML with account, password, game and IP | Authenticates a user game login based on their account information and sends an XML response with their user ID, user type, and success status. If authentication fails, it sends an XML response with a failure status.
|
||||
/serverApi/billing | POST | XML with currency-request or item-purchase-request and associated arguments | Handles billing requests. For currency requests, it retrieves the user's Zen balance from the database and sends an XML response with the balance. For item purchase requests, it deducts the cost of the item from the user's Zen balance and logs the transaction in the database. If the transaction is successful, it sends an XML response with the success status. If the transaction fails, it sends an XML response with a failure status and an error message.
|
||||
/serverApi/gateway | GET | | Returns an XML response containing the IP address and port number of the gateway server.
|
||||
/serverApi/gateway/info | GET | | Returns an response containing the gateway endpoint. Used by the **chn** region.
|
||||
/serverApi/gateway/status | GET | | Checks the status of the gateway server by attempting to establish a connection to the server. Returns a JSON object with the status of the server (online or offline) and an HTTP status code indicating the success or failure of the connection attempt.
|
||||
/accountApi/register | POST | windyCode, email, password | Create a new account with the provided windyCode, email, and password. The password is first combined with the windyCode to create an MD5 hash, which is then salted and hashed again using bcrypt before being stored in the database. An email confirmation is sent to the provided email address, and a success or error message is returned.
|
||||
/accountApi/login | POST | account, password | Authenticates a user account in the launcher by username or email address and password. Return a token if the authentication is successful (currently unsued).
|
||||
/accountApi/login | POST | account, password | Authenticates a user account in the launcher by username or email address and password. Return a token if the authentication is successful (token is currently unsued).
|
||||
/accountApi/codeVerification | POST | email, verification_code_type, verification_code | Verify a user's email by checking the verification code
|
||||
/accountApi/sendPasswordResetEmail | POST | email | Sends an email with a password reset verification code to the specified email address
|
||||
/accountApi/changePassword | POST | email, password, verification_code | Change the password of a user's account, given the email and password verification code
|
||||
/accountApi/sendVerificationEmail | POST | email | Sends a verification email to the specified email address.
|
||||
/launcherApi/launcherUpdater/getLauncherVersion | GET | | Returns the version of the launcher by reading the launcher_info.ini file.
|
||||
/launcherApi/launcherUpdater/updateLauncherVersion | POST | version | Downloads the new version of the launcher from the launcher_update folder.
|
||||
/launcherApi/launcherUpdater/updateLauncherVersion | POST | version | Download the specified launcher versionr from the launcher_update folder.
|
||||
/serverApi/onlineCount | GET | | Returns the number of online players. Returns a JSON object with the count.
|
||||
|
||||
### Preview
|
||||

|
||||
|
|
|
|||
BIN
api.png
BIN
api.png
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 66 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "RustyHearts-API",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Rusty Hearts REST API implementation on node.js",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
|
|
@ -35,7 +35,9 @@
|
|||
"logger": "^0.0.1",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mssql": "^9.1.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.9.1",
|
||||
"pm2": "^5.3.0",
|
||||
"winston": "^3.8.2",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
|
|
|
|||
4
rh-api_with_pm2.bat
Normal file
4
rh-api_with_pm2.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
title API
|
||||
cmd /k "npx pm2 start src/app.js --name rh-api && npx pm2 logs rh-api"
|
||||
pause
|
||||
41
src/app.js
41
src/app.js
|
|
@ -8,6 +8,7 @@ const cors = require('cors');
|
|||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const expressWinston = require('express-winston');
|
||||
const moment = require('moment-timezone');
|
||||
const { logger } = require('./utils/logger');
|
||||
const path = require('path');
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ const passwordResetEmailRouter = require('./routes/launcher/passwordResetEmail')
|
|||
const passwordChangeRouter = require('./routes/launcher/changePassword');
|
||||
const verificationEmailRouter = require('./routes/launcher/verificationEmail');
|
||||
const launcherUpdaterRouter = require('./routes/launcher/launcherUpdater');
|
||||
const onlineCountRouter = require('./routes/onlineCount');
|
||||
|
||||
// Set up rate limiter
|
||||
const limiter = rateLimit({
|
||||
|
|
@ -60,6 +62,7 @@ app.use('/accountApi/sendPasswordResetEmail', limiter , passwordResetEmailRouter
|
|||
app.use('/accountApi/changePassword', limiter , passwordChangeRouter);
|
||||
app.use('/accountApi/sendVerificationEmail', limiter , verificationEmailRouter);
|
||||
app.use('/launcherApi/launcherUpdater', launcherUpdaterRouter);
|
||||
app.use('/serverApi/onlineCount', limiter , onlineCountRouter);
|
||||
|
||||
// Serve static files from public folder
|
||||
app.use(express.static('../public'));
|
||||
|
|
@ -89,12 +92,48 @@ app.use((err, req, res, next) => {
|
|||
} else {
|
||||
logger.info(err.stack);
|
||||
}
|
||||
res.status(500).send('Something went wrong' + err.stack);
|
||||
res.status(500).send('A error ocurred. Try again later.');
|
||||
});
|
||||
|
||||
// Node.js version
|
||||
const nodeVersion = process.version;
|
||||
|
||||
// timezone
|
||||
const timezone = process.env.TZ || new Date().toLocaleString('en-US', { timeZoneName: 'short' });
|
||||
const offsetInMinutes = moment.tz(timezone).utcOffset();
|
||||
const offsetHours = Math.floor(Math.abs(offsetInMinutes) / 60);
|
||||
const offsetMinutes = Math.abs(offsetInMinutes) % 60;
|
||||
const offsetSign = offsetInMinutes >= 0 ? '+' : '-';
|
||||
const offsetString = `${offsetSign}${offsetHours.toString().padStart(2, '0')}:${offsetMinutes.toString().padStart(2, '0')}`;
|
||||
|
||||
const memoryUsage = process.memoryUsage();
|
||||
|
||||
// Function to format bytes as human-readable string
|
||||
function formatBytes(bytes) {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
const i = Math.floor(Math.log2(bytes) / 10);
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
||||
}
|
||||
|
||||
// Start server
|
||||
const port = process.env.PORT || 3000;
|
||||
const publicIP = process.env.PUBLIC_IP || '0.0.0.0';
|
||||
|
||||
console.log('--------------------------------------------------');
|
||||
console.log(`Rusty Hearts API Version: 1.1`)
|
||||
console.log(`Node.js Version: ${nodeVersion}`);
|
||||
console.log(`Timezone: ${timezone} (${offsetString})`);
|
||||
console.log('Memory Usage:');
|
||||
console.log(` RSS: ${formatBytes(memoryUsage.rss)}`);
|
||||
console.log(` Heap Total: ${formatBytes(memoryUsage.heapTotal)}`);
|
||||
console.log(` Heap Used: ${formatBytes(memoryUsage.heapUsed)}`);
|
||||
console.log(` External: ${formatBytes(memoryUsage.external)}`);
|
||||
console.log(` Array Buffers: ${formatBytes(memoryUsage.arrayBuffers)}`);
|
||||
console.log('--------------------------------------------------');
|
||||
|
||||
app.listen(port, publicIP, () => {
|
||||
logger.info(`API listening on ${publicIP}:${port}`);
|
||||
logger.info(`Auth API listening on 127.0.0.1:${authPort}`);
|
||||
|
|
|
|||
|
|
@ -1,77 +1,86 @@
|
|||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const bcrypt = require('bcrypt');
|
||||
const xml2js = require('xml2js');
|
||||
const parser = new xml2js.Parser();
|
||||
const sql = require('mssql');
|
||||
const router = express.Router();
|
||||
const { authLogger } = require('../utils/logger');
|
||||
const Joi = require('joi');
|
||||
|
||||
// Set up database connection
|
||||
const { authLogger } = require('../utils/logger');
|
||||
const { connAccount } = require('../utils/dbConfig');
|
||||
|
||||
const router = express.Router();
|
||||
const parser = new xml2js.Parser({ explicitArray: false });
|
||||
|
||||
// Joi schema for request body validation
|
||||
const schema = Joi.object({
|
||||
'login-request': Joi.object({
|
||||
account: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
game: Joi.string().required(),
|
||||
ip: Joi.string().required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
// Route for handling login requests
|
||||
router.post('/', bodyParser.text({
|
||||
type: '*/xml'
|
||||
}), async (req, res) => {
|
||||
try {
|
||||
const xml = req.body;
|
||||
const result = await parser.parseStringPromise(xml);
|
||||
const loginRequest = result['login-request'];
|
||||
const account = loginRequest.account[0];
|
||||
const password = loginRequest.password[0];
|
||||
const game = loginRequest.game[0];
|
||||
const ip = loginRequest.ip[0];
|
||||
router.post('/', express.text({ type: '*/xml' }), async (req, res) => {
|
||||
try {
|
||||
const xml = req.body;
|
||||
const result = await parser.parseStringPromise(xml);
|
||||
|
||||
authLogger.info(`[Auth] Account [${account}] is trying to login from [${ip}]`);
|
||||
|
||||
// Create a connection pool for the database
|
||||
const pool = await connAccount;
|
||||
|
||||
// Get the account information from the database
|
||||
const {
|
||||
recordset
|
||||
} = await pool
|
||||
.request()
|
||||
.input('Identifier', sql.VarChar(50), account)
|
||||
.execute('GetAccount');
|
||||
|
||||
// Check if the account exists
|
||||
const row = recordset[0];
|
||||
if (!row || row.Result !== 'AccountExists') {
|
||||
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
|
||||
return res.send('<status>failed</status>');
|
||||
}
|
||||
|
||||
// Verify the password using bcrypt
|
||||
const passwordMatch = await bcrypt.compare(password, row.AccountPwd);
|
||||
|
||||
// Authenticate the user and update the database
|
||||
const {
|
||||
recordset: authRecordset
|
||||
} = await pool
|
||||
.request()
|
||||
.input('Identifier', sql.VarChar(50), account)
|
||||
.input('password_verify_result', sql.Bit, passwordMatch ? 1 : 0)
|
||||
.input('LastLoginIP', sql.VarChar(50), ip)
|
||||
.execute('AuthenticateUser');
|
||||
|
||||
const authRow = authRecordset[0];
|
||||
if (!authRow || authRow.Result !== 'LoginSuccess') {
|
||||
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
|
||||
return res.send('<status>failed</status>');
|
||||
}
|
||||
|
||||
// Send the authentication response
|
||||
const response = `<userid>${authRow.AuthID}</userid><user-type>F</user-type><status>success</status>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
res.send(response);
|
||||
|
||||
authLogger.info(`[Auth] Account [${account}] successfully logged in from [${ip}]`);
|
||||
} catch (error) {
|
||||
authLogger.error('Error handling login request: ' + error.message);
|
||||
res.status(500).send('<status>failed</status>');
|
||||
// Validate the request body against the schema
|
||||
const { error, value } = schema.validate(result);
|
||||
if (error) {
|
||||
authLogger.info(`[Auth] Invalid login request: ${error.message}`);
|
||||
return res.send('<status>failed</status>');
|
||||
}
|
||||
|
||||
const { 'login-request': loginRequest } = value;
|
||||
const { account, password, game, ip } = loginRequest;
|
||||
|
||||
authLogger.info(`[Auth] Account [${account}] is trying to login from [${ip}]`);
|
||||
|
||||
// Create a connection pool for the database
|
||||
const pool = await connAccount;
|
||||
|
||||
// Get the account information from the database
|
||||
const { recordset } = await pool
|
||||
.request()
|
||||
.input('Identifier', sql.VarChar(50), account)
|
||||
.execute('GetAccount');
|
||||
|
||||
// Check if the account exists
|
||||
const row = recordset[0];
|
||||
if (!row || row.Result !== 'AccountExists') {
|
||||
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
|
||||
return res.send('<status>failed</status>');
|
||||
}
|
||||
|
||||
// Verify the password using bcrypt
|
||||
const passwordMatch = await bcrypt.compare(password, row.AccountPwd);
|
||||
|
||||
// Authenticate the user and update the database
|
||||
const { recordset: authRecordset } = await pool
|
||||
.request()
|
||||
.input('Identifier', sql.VarChar(50), account)
|
||||
.input('password_verify_result', sql.Bit, passwordMatch ? 1 : 0)
|
||||
.input('LastLoginIP', sql.VarChar(50), ip)
|
||||
.execute('AuthenticateUser');
|
||||
|
||||
const authRow = authRecordset[0];
|
||||
if (!authRow || authRow.Result !== 'LoginSuccess') {
|
||||
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
|
||||
return res.send('<status>failed</status>');
|
||||
}
|
||||
|
||||
// Send the authentication response
|
||||
const response = `<userid>${authRow.AuthID}</userid><user-type>F</user-type><status>success</status>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
res.send(response);
|
||||
|
||||
authLogger.info(`[Auth] Account [${account}] successfully logged in from [${ip}]`);
|
||||
} catch (error) {
|
||||
authLogger.error(`Error handling login request: ${error.message}`);
|
||||
res.status(500).send('<status>failed</status>');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,122 +1,138 @@
|
|||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const xml2js = require('xml2js');
|
||||
const parser = new xml2js.Parser();
|
||||
const sql = require('mssql');
|
||||
const router = express.Router();
|
||||
const { billingLogger} = require('../utils/logger');;
|
||||
const { billingLogger } = require('../utils/logger');
|
||||
const Joi = require('joi');
|
||||
|
||||
// Set up database connection
|
||||
const { connAccount } = require('../utils/dbConfig');
|
||||
|
||||
// Define the validation schema for currency requests
|
||||
const currencySchema = Joi.object({
|
||||
userid: Joi.string().required(),
|
||||
server: Joi.string().required(),
|
||||
game: Joi.string().required(),
|
||||
}).required();
|
||||
|
||||
// Define the validation schema for item purchase requests
|
||||
const itemPurchaseSchema = Joi.object({
|
||||
userid: Joi.string().required(),
|
||||
server: Joi.string().required(),
|
||||
charid: Joi.string().required(),
|
||||
game: Joi.number().required(),
|
||||
uniqueid: Joi.string().required(),
|
||||
amount: Joi.number().required(),
|
||||
itemid: Joi.string().required(),
|
||||
count: Joi.number().required(),
|
||||
}).required();
|
||||
|
||||
|
||||
// Route for handling billing requests
|
||||
router.post('/', bodyParser.text({
|
||||
type: '*/xml'
|
||||
type: '*/xml'
|
||||
}), async (req, res) => {
|
||||
try {
|
||||
const xml = req.body;
|
||||
const result = await parser.parseStringPromise(xml);
|
||||
const name = result['currency-request'] ? 'currency-request' : 'item-purchase-request';
|
||||
const request = result[name];
|
||||
const userid = request.userid[0];
|
||||
const server = request.server[0];
|
||||
try {
|
||||
const xml = req.body;
|
||||
const result = await xml2js.parseStringPromise(xml, { explicitArray: false });
|
||||
const name = result['currency-request'] ? 'currency-request' : 'item-purchase-request';
|
||||
const request = result[name];
|
||||
|
||||
billingLogger.info(`[Billing] Received [${name}] from user [${userid}]`);
|
||||
// Validate the request against the appropriate schema
|
||||
const { error, value } = name === 'currency-request'
|
||||
? currencySchema.validate(request)
|
||||
: itemPurchaseSchema.validate(request);
|
||||
|
||||
// Create a connection pool for the database
|
||||
const pool = await connAccount;
|
||||
|
||||
switch (name) {
|
||||
case 'currency-request':
|
||||
const {
|
||||
recordset
|
||||
} = await pool
|
||||
.request()
|
||||
.input('UserId', sql.VarChar(50), userid)
|
||||
.input('ServerId', sql.VarChar(50), server)
|
||||
.execute('GetCurrency');
|
||||
|
||||
const row = recordset[0];
|
||||
|
||||
if (row && row.Result === 'Success') {
|
||||
const response = `<result><balance>${row.Zen}</balance></result>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
res.send(response);
|
||||
} else {
|
||||
res.send('<status>failed</status>');
|
||||
return res.status(400).send(row.Result);
|
||||
}
|
||||
break;
|
||||
case 'item-purchase-request':
|
||||
const charid = request.charid[0];
|
||||
const uniqueid = request.uniqueid[0];
|
||||
const amount = request.amount[0];
|
||||
const itemid = request.itemid[0];
|
||||
const itemcount = request.count[0];
|
||||
|
||||
const {
|
||||
recordset: currencyRecordset
|
||||
} = await pool
|
||||
.request()
|
||||
.input('UserId', sql.VarChar(50), userid)
|
||||
.input('ServerId', sql.VarChar(50), server)
|
||||
.execute('GetCurrency');
|
||||
|
||||
const currencyRow = currencyRecordset[0];
|
||||
|
||||
if (currencyRow && currencyRow.Result === 'Success') {
|
||||
const balance = currencyRow.Zen;
|
||||
|
||||
if (amount > 0) {
|
||||
if (amount > balance) {
|
||||
res.send('<status>failed</status>');
|
||||
billingLogger.info(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] failed. Not enough Zen [${balance}]. charid: [${charid}] itemid: [${itemid}] itemcount: [${itemcount}] price: [${amount}]`);
|
||||
} else {
|
||||
const newbalance = balance - amount;
|
||||
|
||||
await pool
|
||||
.request()
|
||||
.input('UserId', sql.VarChar(50), userid)
|
||||
.input('ServerId', sql.VarChar(50), server)
|
||||
.input('NewBalance', sql.BigInt, newbalance)
|
||||
.execute('SetCurrency');
|
||||
|
||||
await pool
|
||||
.request()
|
||||
.input('userid', sql.VarChar(50), userid)
|
||||
.input('charid', sql.VarChar(50), charid)
|
||||
.input('uniqueid', sql.VarChar(50), uniqueid)
|
||||
.input('amount', sql.BigInt, amount)
|
||||
.input('itemid', sql.VarChar(50), itemid)
|
||||
.input('itemcount', sql.Int, itemcount)
|
||||
.execute('SetBillingLog');
|
||||
|
||||
billingLogger.info(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] success. charid: [${charid}] itemid: [${itemid}] itemcount: [${itemcount}] price: [${amount}]`);
|
||||
billingLogger.info(`[Billing] Item purchase from user [${userid}] success. New zen balance: [${newbalance}]`);
|
||||
|
||||
const response = `<result><status>success</status><new-balance>${newbalance}</new-balance></result>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
res.send(response);
|
||||
}
|
||||
} else {
|
||||
const response = `<result><balance>${currencyRow.Zen}</balance></result>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
res.send(response);
|
||||
}
|
||||
} else {
|
||||
res.send('<status>failed</status>');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.send('<status>failed</status>');
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
billingLogger.error(`[Billing] Error handling request: $ {
|
||||
error.message
|
||||
}`);
|
||||
res.status(500).send('<status>failed</status>');
|
||||
if (error) {
|
||||
billingLogger.info(`[Billing] Invalid request: ${error.message}`);
|
||||
return res.status(400).send('<status>failed</status>');
|
||||
}
|
||||
|
||||
const { userid, server, game, charid, uniqueid, amount, itemid, count } = value;
|
||||
|
||||
// Create a connection pool for the database
|
||||
const pool = await connAccount;
|
||||
|
||||
switch (name) {
|
||||
case 'currency-request':
|
||||
const { recordset } = await pool
|
||||
.request()
|
||||
.input('UserId', sql.VarChar(50), userid)
|
||||
.input('ServerId', sql.VarChar(50), server)
|
||||
.execute('GetCurrency');
|
||||
|
||||
const row = recordset[0];
|
||||
|
||||
if (row && row.Result === 'Success') {
|
||||
const response = `<result><balance>${row.Zen}</balance></result>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
return res.send(response);
|
||||
} else {
|
||||
billingLogger.error(`[Billing] Currency request from user [${userid}] failed: ${row.Result}`);
|
||||
return res.status(400).send('<status>failed</status>');
|
||||
}
|
||||
|
||||
case 'item-purchase-request':
|
||||
billingLogger.info(`[Billing] Received [${name}] from user [${userid}]`);
|
||||
const { recordset: currencyRecordset } = await pool
|
||||
.request()
|
||||
.input('UserId', sql.VarChar(50), userid)
|
||||
.input('ServerId', sql.VarChar(50), server)
|
||||
.execute('GetCurrency');
|
||||
|
||||
const currencyRow = currencyRecordset[0];
|
||||
|
||||
if (currencyRow && currencyRow.Result === 'Success') {
|
||||
const balance = currencyRow.Zen;
|
||||
|
||||
if (amount > 0) {
|
||||
if (amount > balance) {
|
||||
billingLogger.warn(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] failed. Not enough Zen [${balance}]. charid: [${charid}] itemid: [${itemid}] itemcount: [${count}] price: [${amount}]`);
|
||||
return res.status(400).send('<status>failed</status>');
|
||||
} else {
|
||||
const newbalance = balance - amount;
|
||||
|
||||
await pool
|
||||
.request()
|
||||
.input('UserId', sql.VarChar(50), userid)
|
||||
.input('ServerId', sql.VarChar(50), server)
|
||||
.input('NewBalance', sql.BigInt, newbalance)
|
||||
.execute('SetCurrency');
|
||||
|
||||
await pool
|
||||
.request()
|
||||
.input('userid', sql.VarChar(50), userid)
|
||||
.input('charid', sql.VarChar(50), charid)
|
||||
.input('uniqueid', sql.VarChar(50), uniqueid)
|
||||
.input('amount', sql.BigInt, amount)
|
||||
.input('itemid', sql.VarChar(50), itemid)
|
||||
.input('itemcount', sql.Int, count)
|
||||
.execute('SetBillingLog');
|
||||
|
||||
billingLogger.info(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] success. charid: [${charid}] itemid: [${itemid}] itemcount: [${count}] price: [${amount}]`);
|
||||
billingLogger.info(`[Billing] [${userid}] Zen balance before purchase: [${balance}] | New zen balance: [${newbalance}]`);
|
||||
|
||||
const response = `<result><status>success</status><new-balance>${newbalance}</new-balance></result>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
return res.send(response);
|
||||
}
|
||||
} else {
|
||||
const response = `<result><balance>${currencyRow.Zen}</balance></result>`;
|
||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||
return res.send(response);
|
||||
}
|
||||
} else {
|
||||
return res.status(400).send('<status>failed</status>');
|
||||
}
|
||||
|
||||
default:
|
||||
return res.status(400).send('<status>failed</status>');
|
||||
}
|
||||
} catch (error) {
|
||||
billingLogger.error(`[Billing] Error handling request: ${error.message}`);
|
||||
return res.status(500).send('<status>failed</status>');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const net = require('net');
|
||||
const { logger } = require('../utils/logger');
|
||||
|
||||
// Define the gateway route
|
||||
router.get('/', (req, res) => {
|
||||
|
|
@ -38,14 +39,17 @@ router.get('/status', async (req, res) => {
|
|||
|
||||
// Handle the socket events to check the connection status
|
||||
socket.on('connect', () => {
|
||||
logger.info(`[Gateway] Connection attempt success from IP: ${req.ip}`);
|
||||
res.status(200).json({ status: 'online' });
|
||||
socket.destroy();
|
||||
});
|
||||
socket.on('timeout', () => {
|
||||
logger.warn(`[Gateway] Connection attempt timeout from IP: ${req.ip}`);
|
||||
res.status(408).json({ status: 'offline' });
|
||||
socket.destroy();
|
||||
});
|
||||
socket.on('error', () => {
|
||||
logger.error(`[Gateway] Connection failed from IP: ${req.ip}`);
|
||||
res.status(503).json({ status: 'offline' });
|
||||
socket.destroy();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -100,8 +100,8 @@ router.post('/', async (req, res) => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[Account] Database query failed: ' + error.message);
|
||||
return res.status(500).send('[Account] Database query failed: ' + error.message);
|
||||
logger.error('[Account] A error ocourred: ' + error.message);
|
||||
return res.status(500).send('A error ocourred. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ router.post('/', async (req, res) => {
|
|||
return res.status(200).send(row.Result);
|
||||
} catch (error) {
|
||||
logger.error('Database query failed: ' + error.message);
|
||||
return res.status(500).send('Database query failed: ' + error.message);
|
||||
return res.status(500).send('A error ocourred. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,17 +23,17 @@ router.post('/', async (req, res) => {
|
|||
return res.status(400).send(error.details[0].message);
|
||||
}
|
||||
|
||||
const account = value.account;
|
||||
const password = value.password;
|
||||
const account = req.body.account;
|
||||
const password = req.body.password;
|
||||
const userIp = req.ip;
|
||||
|
||||
// Check the format of the account identifier
|
||||
if (
|
||||
!/^[A-Za-z0-9_-]{6,50}$/.test(account) &&
|
||||
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account)
|
||||
) {
|
||||
return res.status(400).json({ Result: 'InvalidUsernameFormat' });
|
||||
}
|
||||
if (
|
||||
!/^[a-z0-9_-]{6,50}$/.test(account) &&
|
||||
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account)
|
||||
) {
|
||||
return res.status(400).json({ Result: 'InvalidUsernameFormat' });
|
||||
}
|
||||
|
||||
// Use a prepared statement to retrieve the account information
|
||||
const pool = await connAccount;
|
||||
|
|
@ -51,6 +51,7 @@ router.post('/', async (req, res) => {
|
|||
.createHash('md5')
|
||||
.update(windyCode + password)
|
||||
.digest('hex');
|
||||
|
||||
const password_verify_result = await bcrypt.compare(
|
||||
md5_password,
|
||||
hash
|
||||
|
|
@ -85,15 +86,13 @@ router.post('/', async (req, res) => {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
return res.status(400).json({ Result: 'AccountNotFound' });
|
||||
return res.status(400).json({ Result: 'AccountNotFound' });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[Account] Launcher Login: Database query failed: ' + error.message
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.send('Database query failed: ' + error.message);
|
||||
return res.status(500).send('Login failed. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ router.post('/', async (req, res) => {
|
|||
return res.status(200).send('EmailSent');
|
||||
}
|
||||
else {
|
||||
accountLogger.info(`[Account] Failed to insert verification code for email: ${email}`);
|
||||
accountLogger.error(`[Account] Failed to insert verification code for email: ${email}`);
|
||||
return res.status(500).send(insertRow.Result);
|
||||
}
|
||||
} else if (row && row.Result === 'AccountNotFound') {
|
||||
|
|
@ -67,7 +67,7 @@ router.post('/', async (req, res) => {
|
|||
}
|
||||
} catch (error) {
|
||||
logger.error('[Account] Database query failed: ' + error.message);
|
||||
return res.status(500).send('Database query failed: ' + error.message);
|
||||
return res.status(500).send('A error ocourred. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const { connAccount } = require('../../utils/dbConfig');
|
|||
|
||||
// Joi schema for validating request data
|
||||
const schema = Joi.object({
|
||||
windyCode: Joi.string().alphanum().min(1).max(16).required(),
|
||||
windyCode: Joi.string().alphanum().min(6).max(16).required(),
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required(),
|
||||
});
|
||||
|
|
@ -30,6 +30,13 @@ router.post('/', async (req, res) => {
|
|||
const password = value.password;
|
||||
const userIp = req.ip;
|
||||
|
||||
if (
|
||||
!/^[a-z0-9_-]{6,50}$/.test(windyCode) &&
|
||||
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(email)
|
||||
) {
|
||||
return res.status(400).send('InvalidUsernameFormat');
|
||||
}
|
||||
|
||||
const md5_password = crypto.createHash('md5').update(windyCode + password).digest('hex'); // Generate MD5 hash
|
||||
|
||||
const passwordHash = await bcrypt.hash(md5_password, 10);
|
||||
|
|
@ -55,12 +62,12 @@ router.post('/', async (req, res) => {
|
|||
|
||||
return res.status(200).send('Success');
|
||||
} else {
|
||||
accountLogger.info(`[Account] Account [${windyCode}] creation failed: ${row.Result}`);
|
||||
accountLogger.error(`[Account] Account [${windyCode}] creation failed: ${row.Result}`);
|
||||
return res.status(400).send(row.Result);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[Account] Database query failed: ' + error.message);
|
||||
return res.status(500).send('Database query failed: ' + error.message);
|
||||
return res.status(500).send('A error ocourred. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ router.post('/', async (req, res) => {
|
|||
}
|
||||
} catch (error) {
|
||||
logger.error('[Account] Database query failed: ' + error.message);
|
||||
return res.status(500).send('Database query failed: ' + error.message);
|
||||
return res.status(500).send('A error ocourred. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
47
src/routes/onlineCount.js
Normal file
47
src/routes/onlineCount.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const NodeCache = require('node-cache');
|
||||
const { logger } = require('../utils/logger');
|
||||
const sql = require('mssql');
|
||||
const { authDBConfig } = require('../utils/dbConfig.js');
|
||||
|
||||
// Set up the cache
|
||||
const cache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
|
||||
|
||||
// Route for getting the count of online players
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
// Check if the count exists in the cache
|
||||
const cacheKey = 'onlineCount';
|
||||
let count = cache.get(cacheKey);
|
||||
|
||||
if (count === undefined) {
|
||||
// Count not found in cache, fetch it from the database
|
||||
const connAuth = new sql.ConnectionPool(authDBConfig);
|
||||
await connAuth.connect();
|
||||
|
||||
const request = connAuth.request();
|
||||
|
||||
// Declare the @online parameter and set its value to 1
|
||||
request.input('online', sql.Int, 1);
|
||||
|
||||
const result = await request.query('SELECT COUNT(*) AS OnlineCount FROM AuthTable WHERE online = @online');
|
||||
|
||||
count = result.recordset[0].OnlineCount;
|
||||
|
||||
// Store the count in the cache
|
||||
cache.set(cacheKey, count);
|
||||
|
||||
// Close the database connection
|
||||
await connAuth.close();
|
||||
}
|
||||
|
||||
// Return the count as the response
|
||||
return res.status(200).json({ count });
|
||||
} catch (error) {
|
||||
logger.error('Database query failed: ' + error.message);
|
||||
return res.status(500).send('Database query failed. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -9,13 +9,30 @@ const dbConfig = {
|
|||
database: process.env.DB_DATABASE,
|
||||
options: {
|
||||
encrypt: process.env.DB_ENCRYPT === 'true'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const connAccount = new sql.ConnectionPool(dbConfig);
|
||||
connAccount.connect();
|
||||
logger.info(`Database: Connected.`);
|
||||
|
||||
connAccount.connect().then(() => {
|
||||
logger.info(`Account Database: Connected.`);
|
||||
logger.info(`$ Ready $`);
|
||||
|
||||
}).catch((error) => {
|
||||
logger.error(`Failed to connect to the Account Database: ${error}`);
|
||||
});
|
||||
|
||||
const authDBConfig = {
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
server: process.env.DB_SERVER,
|
||||
database: 'RustyHearts_Auth',
|
||||
options: {
|
||||
encrypt: process.env.DB_ENCRYPT === 'true'
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
connAccount
|
||||
connAccount,
|
||||
authDBConfig
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const util = require("util");
|
||||
const winston = require("winston");
|
||||
|
||||
const logsDirectory = 'logs';
|
||||
|
|
@ -9,131 +7,44 @@ if (!fs.existsSync(logsDirectory)) {
|
|||
fs.mkdirSync(logsDirectory);
|
||||
}
|
||||
|
||||
const authLogger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||
),
|
||||
transports: [
|
||||
function createLogger(filename, level, filter, showConsole) {
|
||||
const transports = [
|
||||
new winston.transports.File({
|
||||
filename: `logs/auth-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level: 'info',
|
||||
filter: (log) => log.message.includes('[Auth]')
|
||||
filename: `${logsDirectory}/${filename}-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level,
|
||||
filter
|
||||
})
|
||||
]
|
||||
});
|
||||
];
|
||||
|
||||
if (process.env.LOG_AUTH_CONSOLE === 'true') {
|
||||
authLogger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
const billingLogger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: `logs/billing-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level: 'info',
|
||||
filter: (log) => log.message.includes('[Billing]')
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if (process.env.LOG_BILLING_CONSOLE === 'true') {
|
||||
billingLogger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
const mailerLogger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: `logs/mailer-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level: 'info',
|
||||
filter: (log) => log.message.includes('[Mailer]')
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if (process.env.LOG_MAILER_CONSOLE === 'true') {
|
||||
mailerLogger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
const accountLogger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: `logs/account-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level: 'info',
|
||||
filter: (log) => log.message.includes('[Account]')
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if (process.env.LOG_ACCOUNT_CONSOLE === 'true') {
|
||||
accountLogger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
if (showConsole) {
|
||||
transports.push(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||
)
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: `logs/api-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level: 'info'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: `logs/error-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level: 'error'
|
||||
})
|
||||
]
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||
),
|
||||
transports
|
||||
});
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
const logLevel = process.env.LOG_LEVEL || 'info';
|
||||
|
||||
const authLogger = createLogger('auth', logLevel, log => log.message.includes('[Auth]'), process.env.LOG_AUTH_CONSOLE === 'true');
|
||||
const billingLogger = createLogger('billing', logLevel, log => log.message.includes('[Billing]'), process.env.LOG_BILLING_CONSOLE === 'true');
|
||||
const mailerLogger = createLogger('mailer', logLevel, log => log.message.includes('[Mailer]'), process.env.LOG_MAILER_CONSOLE === 'true');
|
||||
const accountLogger = createLogger('account', logLevel, log => log.message.includes('[Account]'), process.env.LOG_ACCOUNT_CONSOLE === 'true');
|
||||
const logger = createLogger('api', logLevel, null, true);
|
||||
|
||||
module.exports = {
|
||||
authLogger,
|
||||
|
|
|
|||
4
stop_rh-api_with_pm2.bat
Normal file
4
stop_rh-api_with_pm2.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
title API
|
||||
cmd /k "npx pm2 stop rh-api"
|
||||
pause
|
||||
Loading…
Add table
Reference in a new issue