*Added onlineCount endpoint

*Added launch with PM2
*Code clenaup
This commit is contained in:
Junior 2023-07-04 08:25:46 -03:00
parent 5dfa4d83e7
commit b400beea6a
18 changed files with 381 additions and 321 deletions

View file

@ -111,19 +111,20 @@ The api provides the following endpoints:
Endpoint | Method | Arguments | Description 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/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 | 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/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. /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/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/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/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/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. /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/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 ### Preview
![image](api.png) ![image](api.png)

BIN
api.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1,6 +1,6 @@
{ {
"name": "RustyHearts-API", "name": "RustyHearts-API",
"version": "1.0.0", "version": "1.1.0",
"description": "Rusty Hearts REST API implementation on node.js", "description": "Rusty Hearts REST API implementation on node.js",
"main": "src/app.js", "main": "src/app.js",
"scripts": { "scripts": {
@ -35,7 +35,9 @@
"logger": "^0.0.1", "logger": "^0.0.1",
"moment-timezone": "^0.5.43", "moment-timezone": "^0.5.43",
"mssql": "^9.1.1", "mssql": "^9.1.1",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.1", "nodemailer": "^6.9.1",
"pm2": "^5.3.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"xml2js": "^0.6.0" "xml2js": "^0.6.0"
}, },

4
rh-api_with_pm2.bat Normal file
View 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

View file

@ -8,6 +8,7 @@ const cors = require('cors');
const compression = require('compression'); const compression = require('compression');
const rateLimit = require('express-rate-limit'); const rateLimit = require('express-rate-limit');
const expressWinston = require('express-winston'); const expressWinston = require('express-winston');
const moment = require('moment-timezone');
const { logger } = require('./utils/logger'); const { logger } = require('./utils/logger');
const path = require('path'); const path = require('path');
@ -22,6 +23,7 @@ const passwordResetEmailRouter = require('./routes/launcher/passwordResetEmail')
const passwordChangeRouter = require('./routes/launcher/changePassword'); const passwordChangeRouter = require('./routes/launcher/changePassword');
const verificationEmailRouter = require('./routes/launcher/verificationEmail'); const verificationEmailRouter = require('./routes/launcher/verificationEmail');
const launcherUpdaterRouter = require('./routes/launcher/launcherUpdater'); const launcherUpdaterRouter = require('./routes/launcher/launcherUpdater');
const onlineCountRouter = require('./routes/onlineCount');
// Set up rate limiter // Set up rate limiter
const limiter = rateLimit({ const limiter = rateLimit({
@ -60,6 +62,7 @@ app.use('/accountApi/sendPasswordResetEmail', limiter , passwordResetEmailRouter
app.use('/accountApi/changePassword', limiter , passwordChangeRouter); app.use('/accountApi/changePassword', limiter , passwordChangeRouter);
app.use('/accountApi/sendVerificationEmail', limiter , verificationEmailRouter); app.use('/accountApi/sendVerificationEmail', limiter , verificationEmailRouter);
app.use('/launcherApi/launcherUpdater', launcherUpdaterRouter); app.use('/launcherApi/launcherUpdater', launcherUpdaterRouter);
app.use('/serverApi/onlineCount', limiter , onlineCountRouter);
// Serve static files from public folder // Serve static files from public folder
app.use(express.static('../public')); app.use(express.static('../public'));
@ -89,14 +92,50 @@ app.use((err, req, res, next) => {
} else { } else {
logger.info(err.stack); 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 // Start server
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const publicIP = process.env.PUBLIC_IP || '0.0.0.0'; 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, () => { app.listen(port, publicIP, () => {
logger.info(`API listening on ${publicIP}:${port}`); logger.info(`API listening on ${publicIP}:${port}`);
logger.info(`Auth API listening on 127.0.0.1:${authPort}`); logger.info(`Auth API listening on 127.0.0.1:${authPort}`);
logger.info(`Billing API listening on 127.0.0.1:${billingPort}`); logger.info(`Billing API listening on 127.0.0.1:${billingPort}`);
}); });

View file

@ -1,77 +1,86 @@
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const xml2js = require('xml2js'); const xml2js = require('xml2js');
const parser = new xml2js.Parser();
const sql = require('mssql'); const sql = require('mssql');
const router = express.Router(); const Joi = require('joi');
const { authLogger } = require('../utils/logger');
// Set up database connection const { authLogger } = require('../utils/logger');
const { connAccount } = require('../utils/dbConfig'); const { connAccount } = require('../utils/dbConfig');
// Route for handling login requests const router = express.Router();
router.post('/', bodyParser.text({ const parser = new xml2js.Parser({ explicitArray: false });
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];
authLogger.info(`[Auth] Account [${account}] is trying to login from [${ip}]`); // Joi schema for request body validation
const schema = Joi.object({
// Create a connection pool for the database 'login-request': Joi.object({
const pool = await connAccount; account: Joi.string().required(),
password: Joi.string().required(),
// Get the account information from the database game: Joi.string().required(),
const { ip: Joi.string().required(),
recordset }).required(),
} = 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; // 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('<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;

View file

@ -1,122 +1,138 @@
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const xml2js = require('xml2js'); const xml2js = require('xml2js');
const parser = new xml2js.Parser();
const sql = require('mssql'); const sql = require('mssql');
const router = express.Router(); const router = express.Router();
const { billingLogger} = require('../utils/logger');; const { billingLogger } = require('../utils/logger');
const Joi = require('joi');
// Set up database connection // Set up database connection
const { connAccount } = require('../utils/dbConfig'); 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 // Route for handling billing requests
router.post('/', bodyParser.text({ router.post('/', bodyParser.text({
type: '*/xml' type: '*/xml'
}), async (req, res) => { }), async (req, res) => {
try { try {
const xml = req.body; const xml = req.body;
const result = await parser.parseStringPromise(xml); const result = await xml2js.parseStringPromise(xml, { explicitArray: false });
const name = result['currency-request'] ? 'currency-request' : 'item-purchase-request'; const name = result['currency-request'] ? 'currency-request' : 'item-purchase-request';
const request = result[name]; const request = result[name];
const userid = request.userid[0];
const server = request.server[0];
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 if (error) {
const pool = await connAccount; billingLogger.info(`[Billing] Invalid request: ${error.message}`);
return res.status(400).send('<status>failed</status>');
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>');
} }
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; module.exports = router;

View file

@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const net = require('net'); const net = require('net');
const { logger } = require('../utils/logger');
// Define the gateway route // Define the gateway route
router.get('/', (req, res) => { router.get('/', (req, res) => {
@ -38,14 +39,17 @@ router.get('/status', async (req, res) => {
// Handle the socket events to check the connection status // Handle the socket events to check the connection status
socket.on('connect', () => { socket.on('connect', () => {
logger.info(`[Gateway] Connection attempt success from IP: ${req.ip}`);
res.status(200).json({ status: 'online' }); res.status(200).json({ status: 'online' });
socket.destroy(); socket.destroy();
}); });
socket.on('timeout', () => { socket.on('timeout', () => {
logger.warn(`[Gateway] Connection attempt timeout from IP: ${req.ip}`);
res.status(408).json({ status: 'offline' }); res.status(408).json({ status: 'offline' });
socket.destroy(); socket.destroy();
}); });
socket.on('error', () => { socket.on('error', () => {
logger.error(`[Gateway] Connection failed from IP: ${req.ip}`);
res.status(503).json({ status: 'offline' }); res.status(503).json({ status: 'offline' });
socket.destroy(); socket.destroy();
}); });

View file

@ -100,8 +100,8 @@ router.post('/', async (req, res) => {
} }
} catch (error) { } catch (error) {
logger.error('[Account] Database query failed: ' + error.message); logger.error('[Account] A error ocourred: ' + error.message);
return res.status(500).send('[Account] Database query failed: ' + error.message); return res.status(500).send('A error ocourred. Please try again later.');
} }
}); });

View file

@ -46,7 +46,7 @@ router.post('/', async (req, res) => {
return res.status(200).send(row.Result); return res.status(200).send(row.Result);
} catch (error) { } catch (error) {
logger.error('Database query failed: ' + error.message); 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.');
} }
}); });

View file

@ -23,17 +23,17 @@ router.post('/', async (req, res) => {
return res.status(400).send(error.details[0].message); return res.status(400).send(error.details[0].message);
} }
const account = value.account; const account = req.body.account;
const password = value.password; const password = req.body.password;
const userIp = req.ip; const userIp = req.ip;
// Check the format of the account identifier // Check the format of the account identifier
if ( if (
!/^[A-Za-z0-9_-]{6,50}$/.test(account) && !/^[a-z0-9_-]{6,50}$/.test(account) &&
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account) !/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account)
) { ) {
return res.status(400).json({ Result: 'InvalidUsernameFormat' }); return res.status(400).json({ Result: 'InvalidUsernameFormat' });
} }
// Use a prepared statement to retrieve the account information // Use a prepared statement to retrieve the account information
const pool = await connAccount; const pool = await connAccount;
@ -51,6 +51,7 @@ router.post('/', async (req, res) => {
.createHash('md5') .createHash('md5')
.update(windyCode + password) .update(windyCode + password)
.digest('hex'); .digest('hex');
const password_verify_result = await bcrypt.compare( const password_verify_result = await bcrypt.compare(
md5_password, md5_password,
hash hash
@ -85,15 +86,13 @@ router.post('/', async (req, res) => {
}); });
} }
} else { } else {
return res.status(400).json({ Result: 'AccountNotFound' }); return res.status(400).json({ Result: 'AccountNotFound' });
} }
} catch (error) { } catch (error) {
logger.error( logger.error(
'[Account] Launcher Login: Database query failed: ' + error.message '[Account] Launcher Login: Database query failed: ' + error.message
); );
return res return res.status(500).send('Login failed. Please try again later.');
.status(500)
.send('Database query failed: ' + error.message);
} }
}); });

View file

@ -57,7 +57,7 @@ router.post('/', async (req, res) => {
return res.status(200).send('EmailSent'); return res.status(200).send('EmailSent');
} }
else { 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); return res.status(500).send(insertRow.Result);
} }
} else if (row && row.Result === 'AccountNotFound') { } else if (row && row.Result === 'AccountNotFound') {
@ -67,7 +67,7 @@ router.post('/', async (req, res) => {
} }
} catch (error) { } catch (error) {
logger.error('[Account] Database query failed: ' + error.message); 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.');
} }
}); });

View file

@ -12,7 +12,7 @@ const { connAccount } = require('../../utils/dbConfig');
// Joi schema for validating request data // Joi schema for validating request data
const schema = Joi.object({ 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(), email: Joi.string().email().required(),
password: Joi.string().min(6).required(), password: Joi.string().min(6).required(),
}); });
@ -29,6 +29,13 @@ router.post('/', async (req, res) => {
const email = value.email; const email = value.email;
const password = value.password; const password = value.password;
const userIp = req.ip; 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 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'); return res.status(200).send('Success');
} else { } 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); return res.status(400).send(row.Result);
} }
} catch (error) { } catch (error) {
logger.error('[Account] Database query failed: ' + error.message); 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.');
} }
}); });

View file

@ -61,7 +61,7 @@ router.post('/', async (req, res) => {
} }
} catch (error) { } catch (error) {
logger.error('[Account] Database query failed: ' + error.message); 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
View 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;

View file

@ -9,13 +9,30 @@ const dbConfig = {
database: process.env.DB_DATABASE, database: process.env.DB_DATABASE,
options: { options: {
encrypt: process.env.DB_ENCRYPT === 'true' encrypt: process.env.DB_ENCRYPT === 'true'
} },
}; };
const connAccount = new sql.ConnectionPool(dbConfig); 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 = { module.exports = {
connAccount connAccount,
authDBConfig
}; };

View file

@ -1,6 +1,4 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path");
const util = require("util");
const winston = require("winston"); const winston = require("winston");
const logsDirectory = 'logs'; const logsDirectory = 'logs';
@ -9,131 +7,44 @@ if (!fs.existsSync(logsDirectory)) {
fs.mkdirSync(logsDirectory); fs.mkdirSync(logsDirectory);
} }
const authLogger = winston.createLogger({ function createLogger(filename, level, filter, showConsole) {
level: 'info', const transports = [
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({ new winston.transports.File({
filename: `logs/auth-${new Date().toISOString().slice(0, 10)}.log`, filename: `${logsDirectory}/${filename}-${new Date().toISOString().slice(0, 10)}.log`,
level: 'info', level,
filter: (log) => log.message.includes('[Auth]') filter
}) })
] ];
});
if (process.env.LOG_AUTH_CONSOLE === 'true') { if (showConsole) {
authLogger.add(new winston.transports.Console({ 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}]`)
)
}));
}
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({
format: winston.format.combine( format: winston.format.combine(
winston.format.colorize(), winston.format.colorize(),
winston.format.simple(), winston.format.simple(),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`) 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 = { module.exports = {
authLogger, authLogger,

4
stop_rh-api_with_pm2.bat Normal file
View file

@ -0,0 +1,4 @@
@echo off
title API
cmd /k "npx pm2 stop rh-api"
pause