diff --git a/README.md b/README.md
index f547f73..8b57b39 100644
--- a/README.md
+++ b/README.md
@@ -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

diff --git a/api.png b/api.png
index 29e6553..8a4e2df 100644
Binary files a/api.png and b/api.png differ
diff --git a/package.json b/package.json
index ff65e3e..b8f8f70 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/rh-api_with_pm2.bat b/rh-api_with_pm2.bat
new file mode 100644
index 0000000..c43c82a
--- /dev/null
+++ b/rh-api_with_pm2.bat
@@ -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
diff --git a/src/app.js b/src/app.js
index 0af9e87..55910a3 100644
--- a/src/app.js
+++ b/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,14 +92,50 @@ 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}`);
logger.info(`Billing API listening on 127.0.0.1:${billingPort}`);
-});
\ No newline at end of file
+});
diff --git a/src/routes/auth.js b/src/routes/auth.js
index 6882198..45422df 100644
--- a/src/routes/auth.js
+++ b/src/routes/auth.js
@@ -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');
-// 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];
+const router = express.Router();
+const parser = new xml2js.Parser({ explicitArray: false });
- 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('failed');
- }
-
- // 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('failed');
- }
-
- // Send the authentication response
- const response = `${authRow.AuthID}Fsuccess`;
- 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('failed');
- }
+// 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(),
});
-module.exports = router;
\ No newline at end of file
+// Route for handling login requests
+router.post('/', express.text({ type: '*/xml' }), async (req, res) => {
+ try {
+ const xml = req.body;
+ const result = await parser.parseStringPromise(xml);
+
+ // 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('failed');
+ }
+
+ 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('failed');
+ }
+
+ // 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('failed');
+ }
+
+ // Send the authentication response
+ const response = `${authRow.AuthID}Fsuccess`;
+ 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('failed');
+ }
+});
+
+module.exports = router;
diff --git a/src/routes/billing.js b/src/routes/billing.js
index 1272e42..7f9a9ba 100644
--- a/src/routes/billing.js
+++ b/src/routes/billing.js
@@ -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 = `${row.Zen}`;
- res.set('Content-Type', 'text/xml; charset=utf-8');
- res.send(response);
- } else {
- res.send('failed');
- 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('failed');
- 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 = `success${newbalance}`;
- res.set('Content-Type', 'text/xml; charset=utf-8');
- res.send(response);
- }
- } else {
- const response = `${currencyRow.Zen}`;
- res.set('Content-Type', 'text/xml; charset=utf-8');
- res.send(response);
- }
- } else {
- res.send('failed');
- }
- break;
- default:
- res.send('failed');
- break;
- }
- } catch (error) {
- billingLogger.error(`[Billing] Error handling request: $ {
- error.message
- }`);
- res.status(500).send('failed');
+ if (error) {
+ billingLogger.info(`[Billing] Invalid request: ${error.message}`);
+ return res.status(400).send('failed');
}
+
+ 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 = `${row.Zen}`;
+ 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('failed');
+ }
+
+ 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('failed');
+ } 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 = `success${newbalance}`;
+ res.set('Content-Type', 'text/xml; charset=utf-8');
+ return res.send(response);
+ }
+ } else {
+ const response = `${currencyRow.Zen}`;
+ res.set('Content-Type', 'text/xml; charset=utf-8');
+ return res.send(response);
+ }
+ } else {
+ return res.status(400).send('failed');
+ }
+
+ default:
+ return res.status(400).send('failed');
+ }
+ } catch (error) {
+ billingLogger.error(`[Billing] Error handling request: ${error.message}`);
+ return res.status(500).send('failed');
+ }
});
-module.exports = router;
\ No newline at end of file
+module.exports = router;
diff --git a/src/routes/gateway.js b/src/routes/gateway.js
index 3772ae1..69b20e7 100644
--- a/src/routes/gateway.js
+++ b/src/routes/gateway.js
@@ -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();
});
diff --git a/src/routes/launcher/changePassword.js b/src/routes/launcher/changePassword.js
index 185df6e..8ee8a51 100644
--- a/src/routes/launcher/changePassword.js
+++ b/src/routes/launcher/changePassword.js
@@ -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.');
}
});
diff --git a/src/routes/launcher/codeVerification.js b/src/routes/launcher/codeVerification.js
index 20ac433..93c0458 100644
--- a/src/routes/launcher/codeVerification.js
+++ b/src/routes/launcher/codeVerification.js
@@ -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.');
}
});
diff --git a/src/routes/launcher/login.js b/src/routes/launcher/login.js
index e0e957b..da83906 100644
--- a/src/routes/launcher/login.js
+++ b/src/routes/launcher/login.js
@@ -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.');
}
});
diff --git a/src/routes/launcher/passwordResetEmail.js b/src/routes/launcher/passwordResetEmail.js
index 8bdddd6..cdcdd02 100644
--- a/src/routes/launcher/passwordResetEmail.js
+++ b/src/routes/launcher/passwordResetEmail.js
@@ -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.');
}
});
diff --git a/src/routes/launcher/register.js b/src/routes/launcher/register.js
index 3bfe745..57ebcb9 100644
--- a/src/routes/launcher/register.js
+++ b/src/routes/launcher/register.js
@@ -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(),
});
@@ -29,6 +29,13 @@ router.post('/', async (req, res) => {
const email = value.email;
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
@@ -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.');
}
});
diff --git a/src/routes/launcher/verificationEmail.js b/src/routes/launcher/verificationEmail.js
index 0c49dd7..d8932c9 100644
--- a/src/routes/launcher/verificationEmail.js
+++ b/src/routes/launcher/verificationEmail.js
@@ -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.');
}
});
diff --git a/src/routes/onlineCount.js b/src/routes/onlineCount.js
new file mode 100644
index 0000000..74c8cbb
--- /dev/null
+++ b/src/routes/onlineCount.js
@@ -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;
diff --git a/src/utils/dbConfig.js b/src/utils/dbConfig.js
index 5c11435..8d6468a 100644
--- a/src/utils/dbConfig.js
+++ b/src/utils/dbConfig.js
@@ -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
};
diff --git a/src/utils/logger.js b/src/utils/logger.js
index f0b29e7..4b66136 100644
--- a/src/utils/logger.js
+++ b/src/utils/logger.js
@@ -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,
diff --git a/stop_rh-api_with_pm2.bat b/stop_rh-api_with_pm2.bat
new file mode 100644
index 0000000..77351b0
--- /dev/null
+++ b/stop_rh-api_with_pm2.bat
@@ -0,0 +1,4 @@
+@echo off
+title API
+cmd /k "npx pm2 stop rh-api"
+pause