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

|

|
||||||
|
|
|
||||||
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",
|
"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
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 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,12 +92,48 @@ 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}`);
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,40 @@
|
||||||
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');
|
||||||
|
|
||||||
|
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
|
// Route for handling login requests
|
||||||
router.post('/', bodyParser.text({
|
router.post('/', express.text({ type: '*/xml' }), async (req, res) => {
|
||||||
type: '*/xml'
|
|
||||||
}), async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const xml = req.body;
|
const xml = req.body;
|
||||||
const result = await parser.parseStringPromise(xml);
|
const result = await parser.parseStringPromise(xml);
|
||||||
const loginRequest = result['login-request'];
|
|
||||||
const account = loginRequest.account[0];
|
// Validate the request body against the schema
|
||||||
const password = loginRequest.password[0];
|
const { error, value } = schema.validate(result);
|
||||||
const game = loginRequest.game[0];
|
if (error) {
|
||||||
const ip = loginRequest.ip[0];
|
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}]`);
|
authLogger.info(`[Auth] Account [${account}] is trying to login from [${ip}]`);
|
||||||
|
|
||||||
|
|
@ -29,9 +42,7 @@ router.post('/', bodyParser.text({
|
||||||
const pool = await connAccount;
|
const pool = await connAccount;
|
||||||
|
|
||||||
// Get the account information from the database
|
// Get the account information from the database
|
||||||
const {
|
const { recordset } = await pool
|
||||||
recordset
|
|
||||||
} = await pool
|
|
||||||
.request()
|
.request()
|
||||||
.input('Identifier', sql.VarChar(50), account)
|
.input('Identifier', sql.VarChar(50), account)
|
||||||
.execute('GetAccount');
|
.execute('GetAccount');
|
||||||
|
|
@ -47,9 +58,7 @@ router.post('/', bodyParser.text({
|
||||||
const passwordMatch = await bcrypt.compare(password, row.AccountPwd);
|
const passwordMatch = await bcrypt.compare(password, row.AccountPwd);
|
||||||
|
|
||||||
// Authenticate the user and update the database
|
// Authenticate the user and update the database
|
||||||
const {
|
const { recordset: authRecordset } = await pool
|
||||||
recordset: authRecordset
|
|
||||||
} = await pool
|
|
||||||
.request()
|
.request()
|
||||||
.input('Identifier', sql.VarChar(50), account)
|
.input('Identifier', sql.VarChar(50), account)
|
||||||
.input('password_verify_result', sql.Bit, passwordMatch ? 1 : 0)
|
.input('password_verify_result', sql.Bit, passwordMatch ? 1 : 0)
|
||||||
|
|
@ -69,7 +78,7 @@ router.post('/', bodyParser.text({
|
||||||
|
|
||||||
authLogger.info(`[Auth] Account [${account}] successfully logged in from [${ip}]`);
|
authLogger.info(`[Auth] Account [${account}] successfully logged in from [${ip}]`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authLogger.error('Error handling login request: ' + error.message);
|
authLogger.error(`Error handling login request: ${error.message}`);
|
||||||
res.status(500).send('<status>failed</status>');
|
res.status(500).send('<status>failed</status>');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,62 @@
|
||||||
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);
|
||||||
|
|
||||||
|
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
|
// Create a connection pool for the database
|
||||||
const pool = await connAccount;
|
const pool = await connAccount;
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'currency-request':
|
case 'currency-request':
|
||||||
const {
|
const { recordset } = await pool
|
||||||
recordset
|
|
||||||
} = await pool
|
|
||||||
.request()
|
.request()
|
||||||
.input('UserId', sql.VarChar(50), userid)
|
.input('UserId', sql.VarChar(50), userid)
|
||||||
.input('ServerId', sql.VarChar(50), server)
|
.input('ServerId', sql.VarChar(50), server)
|
||||||
|
|
@ -41,22 +67,15 @@ router.post('/', bodyParser.text({
|
||||||
if (row && row.Result === 'Success') {
|
if (row && row.Result === 'Success') {
|
||||||
const response = `<result><balance>${row.Zen}</balance></result>`;
|
const response = `<result><balance>${row.Zen}</balance></result>`;
|
||||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||||
res.send(response);
|
return res.send(response);
|
||||||
} else {
|
} else {
|
||||||
res.send('<status>failed</status>');
|
billingLogger.error(`[Billing] Currency request from user [${userid}] failed: ${row.Result}`);
|
||||||
return res.status(400).send(row.Result);
|
return res.status(400).send('<status>failed</status>');
|
||||||
}
|
}
|
||||||
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 {
|
case 'item-purchase-request':
|
||||||
recordset: currencyRecordset
|
billingLogger.info(`[Billing] Received [${name}] from user [${userid}]`);
|
||||||
} = await pool
|
const { recordset: currencyRecordset } = await pool
|
||||||
.request()
|
.request()
|
||||||
.input('UserId', sql.VarChar(50), userid)
|
.input('UserId', sql.VarChar(50), userid)
|
||||||
.input('ServerId', sql.VarChar(50), server)
|
.input('ServerId', sql.VarChar(50), server)
|
||||||
|
|
@ -69,8 +88,8 @@ router.post('/', bodyParser.text({
|
||||||
|
|
||||||
if (amount > 0) {
|
if (amount > 0) {
|
||||||
if (amount > balance) {
|
if (amount > balance) {
|
||||||
res.send('<status>failed</status>');
|
billingLogger.warn(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] failed. Not enough Zen [${balance}]. charid: [${charid}] itemid: [${itemid}] itemcount: [${count}] price: [${amount}]`);
|
||||||
billingLogger.info(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] failed. Not enough Zen [${balance}]. charid: [${charid}] itemid: [${itemid}] itemcount: [${itemcount}] price: [${amount}]`);
|
return res.status(400).send('<status>failed</status>');
|
||||||
} else {
|
} else {
|
||||||
const newbalance = balance - amount;
|
const newbalance = balance - amount;
|
||||||
|
|
||||||
|
|
@ -88,34 +107,31 @@ router.post('/', bodyParser.text({
|
||||||
.input('uniqueid', sql.VarChar(50), uniqueid)
|
.input('uniqueid', sql.VarChar(50), uniqueid)
|
||||||
.input('amount', sql.BigInt, amount)
|
.input('amount', sql.BigInt, amount)
|
||||||
.input('itemid', sql.VarChar(50), itemid)
|
.input('itemid', sql.VarChar(50), itemid)
|
||||||
.input('itemcount', sql.Int, itemcount)
|
.input('itemcount', sql.Int, count)
|
||||||
.execute('SetBillingLog');
|
.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 with id [${uniqueid}] from user [${userid}] success. charid: [${charid}] itemid: [${itemid}] itemcount: [${count}] price: [${amount}]`);
|
||||||
billingLogger.info(`[Billing] Item purchase from user [${userid}] success. New zen balance: [${newbalance}]`);
|
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>`;
|
const response = `<result><status>success</status><new-balance>${newbalance}</new-balance></result>`;
|
||||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||||
res.send(response);
|
return res.send(response);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const response = `<result><balance>${currencyRow.Zen}</balance></result>`;
|
const response = `<result><balance>${currencyRow.Zen}</balance></result>`;
|
||||||
res.set('Content-Type', 'text/xml; charset=utf-8');
|
res.set('Content-Type', 'text/xml; charset=utf-8');
|
||||||
res.send(response);
|
return res.send(response);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.send('<status>failed</status>');
|
return res.status(400).send('<status>failed</status>');
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
res.send('<status>failed</status>');
|
return res.status(400).send('<status>failed</status>');
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
billingLogger.error(`[Billing] Error handling request: $ {
|
billingLogger.error(`[Billing] Error handling request: ${error.message}`);
|
||||||
error.message
|
return res.status(500).send('<status>failed</status>');
|
||||||
}`);
|
|
||||||
res.status(500).send('<status>failed</status>');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ 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' });
|
||||||
|
|
@ -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
|
||||||
|
|
@ -91,9 +92,7 @@ router.post('/', async (req, res) => {
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
@ -30,6 +30,13 @@ router.post('/', async (req, res) => {
|
||||||
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
|
||||||
|
|
||||||
const passwordHash = await bcrypt.hash(md5_password, 10);
|
const passwordHash = await bcrypt.hash(md5_password, 10);
|
||||||
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
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,
|
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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(
|
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}]`)
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const billingLogger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: 'info',
|
level,
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||||
),
|
),
|
||||||
transports: [
|
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') {
|
return logger;
|
||||||
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({
|
const logLevel = process.env.LOG_LEVEL || 'info';
|
||||||
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(
|
|
||||||
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 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
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