Add project files.

This commit is contained in:
Junior 2023-05-12 17:44:41 -03:00
commit 0a12c6baa0
41 changed files with 2698 additions and 0 deletions

101
src/app.js Normal file
View file

@ -0,0 +1,101 @@
// Load environment variables
const env = require('./utils/env');
// Import modules
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const expressWinston = require('express-winston');
const { logger } = require('./utils/logger');
const path = require('path');
// Import routes
const authRouter = require('./routes/auth');
const billingRouter = require('./routes/billing');
const gatewayRouter = require('./routes/gateway');
const loginRouter = require('./routes/launcher/login');
const registerRouter = require('./routes/launcher/register');
const codeVerificationRouter = require('./routes/launcher/codeVerification');
const passwordResetEmailRouter = require('./routes/launcher/passwordResetEmail');
const passwordChangeRouter = require('./routes/launcher/changePassword');
const verificationEmailRouter = require('./routes/launcher/verificationEmail');
const launcherUpdaterRouter = require('./routes/launcher/launcherUpdater');
// Set up rate limiter
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // limit each IP to 60 requests per minute
message: 'Too many requests from this IP, please try again later'
});
const app = express();
// Set up middleware
const middleware = [
cors(),
compression(),
express.json(),
express.urlencoded({ extended: false }),
];
if (process.env.ENABLE_HELMET === 'true') {
middleware.unshift(helmet());
}
app.use(...middleware);
const authPort = process.env.AUTH_PORT || 8070;
const billingPort = process.env.BILLING_PORT || 8080;
// Set up routes
app.use('/serverApi/auth', limiter, authRouter).listen(authPort, '127.0.0.1');
app.use('/serverApi/billing', limiter , billingRouter).listen(billingPort, '127.0.0.1');
app.use('/serverApi/gateway', limiter , gatewayRouter);
app.use('/accountApi/register', limiter , registerRouter);
app.use('/accountApi/login', limiter , loginRouter);
app.use('/accountApi/codeVerification', limiter , codeVerificationRouter);
app.use('/accountApi/sendPasswordResetEmail', limiter , passwordResetEmailRouter);
app.use('/accountApi/changePassword', limiter , passwordChangeRouter);
app.use('/accountApi/sendVerificationEmail', limiter , verificationEmailRouter);
app.use('/launcherApi/launcherUpdater', launcherUpdaterRouter);
// Serve static files from public folder
app.use(express.static('../public'));
// Serve static files for the launcher
app.get('/launcher/news', (req, res) => {
res.sendFile(path.join(__dirname, '../public/launcher/news/news-panel.html'));
});
app.get('/launcher/agreement', (req, res) => {
res.sendFile(path.join(__dirname, '../public/launcher/news/agreement.html'));
});
app.get('/favicon.ico', (req, res) => {
res.sendFile(path.join(__dirname, '../public/launcher/news/favicon.ico'));
});
app.use('/launcher/news/images', express.static(path.join(__dirname, '../public/launcher/news/images')));
app.use('/launcher/news', express.static(path.join(__dirname, '../public/launcher/news')));
app.use('/launcher/patch', express.static(path.join(__dirname, '../public/launcher/patch')));
// Set up error handling middleware
app.use((err, req, res, next) => {
if (env.LOG_LEVEL && env.LOG_LEVEL === 'error') {
logger.error(err.stack);
} else {
logger.info(err.stack);
}
res.status(500).send('Something went wrong' + err.stack);
});
// Start server
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => {
logger.info(`API listening on *:${port}`);
logger.info(`Auth API listening on 127.0.0.1:${authPort}`);
logger.info(`Billing API listening on 127.0.0.1:${billingPort}`);
});

114
src/mailer/mailer.js Normal file
View file

@ -0,0 +1,114 @@
const nodemailer = require('nodemailer');
const handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
const { mailerLogger } = require('../utils/logger');
// Load the email templates
const emailTemplates = {
confirmation: fs.readFileSync(path.join(__dirname, 'templates', 'confirmationTemplate.hbs'), 'utf-8'),
verification: fs.readFileSync(path.join(__dirname, 'templates', 'verificationTemplate.hbs'), 'utf-8'),
passwordReset: fs.readFileSync(path.join(__dirname, 'templates', 'passwordResetTemplate.hbs'), 'utf-8'),
passwordChanged: fs.readFileSync(path.join(__dirname, 'templates', 'passwordChangedTemplate.hbs'), 'utf-8')
};
// Compile the email templates
const compiledTemplates = {
confirmation: handlebars.compile(emailTemplates.confirmation),
verification: handlebars.compile(emailTemplates.verification),
passwordReset: handlebars.compile(emailTemplates.passwordReset),
passwordChanged: handlebars.compile(emailTemplates.passwordChanged)
};
// SMTP transport configuration
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_ENCRYPTION === 'ssl' || process.env.SMTP_ENCRYPTION === 'tls',
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD
}
});
function sendConfirmationEmail(email, windyCode) {
const template = compiledTemplates.confirmation;
const emailContent = template({ windyCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
to: email,
subject: '[Rusty Hearts] Account Creation Confirmation',
html: emailContent
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
mailerLogger.error('[Mailer] Error sending confirmation email: ' + error.message);
} else {
mailerLogger.info('[Mailer] Confirmation email sent: ' + info.response);
}
});
}
function sendVerificationEmail(email, verificationCode) {
const template = compiledTemplates.verification;
const emailContent = template({ verificationCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
to: email,
subject: '[Rusty Hearts] Account Creation',
html: emailContent
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
mailerLogger.error('[Mailer] Error sending verification email: ' + error.message);
} else {
mailerLogger.info('[Mailer] Verification email sent: ' + info.response);
}
});
}
function sendPasswordResetEmail(email, verificationCode) {
const template = compiledTemplates.passwordReset;
const emailContent = template({ verificationCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
to: email,
subject: '[Rusty Hearts] Password Reset Request',
html: emailContent
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
mailerLogger.error('[Mailer] Error sending password reset email: ' + error.message);
} else {
mailerLogger.info('[Mailer] Password reset email sent: ' + info.response);
}
});
}
function sendPasswordChangedEmail(email, windyCode) {
const template = compiledTemplates.passwordChanged;
const emailContent = template({ windyCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
to: email,
subject: '[Rusty Hearts] Account Password Changed',
html: emailContent
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
mailerLogger.error('[Mailer] Error sending password changed email: ' + error.message);
} else {
mailerLogger.info('[Mailer] Password changed email sent: ' + info.response);
}
});
}
module.exports = {sendConfirmationEmail, sendVerificationEmail, sendPasswordResetEmail, sendPasswordChangedEmail};

View file

@ -0,0 +1,14 @@
<html>
<head>
<title>Welcome to Rusty Hearts!</title>
</head>
<body>
<h1>Welcome to Rusty Hearts</h1>
<p>Dear {{windyCode}},</p>
<p>Thank you for creating an account with Rusty Hearts! We are thrilled to have you as part of our community.</p>
<p>You are now ready to login and start playing Rusty Hearts. As you embark on your journey, remember to have fun and enjoy the game!</p>
<p>If you have any questions or need assistance with anything, please do not hesitate to contact our support team. We are always here to help you.</p>
<p>Thank you again for choosing Rusty Hearts. We look forward to seeing you in the game!</p>
<p>Best regards,<br>Rusty Hearts Team</p>
</body>
</html>

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>[Rusty Hearts] Account Password Changed</title>
</head>
<body>
<p>Hello, {{windyCode}}</p>
<p>We're writing to let you know that the password for your Rusty Hearts account has been changed. If you made this change yourself, you can disregard this message.</p>
<p>However, if you didn't change your password or if you're unsure if someone else has gained access to your account, we recommend taking immediate action to secure your account. Here are some steps you can take:</p>
<ul>
<li>Change your password again and make sure it's strong and unique</li>
<li>Contact our support team if you need further assistance or if you believe your account has been compromised</li>
</ul>
<p>Best regards,<br>Rusty Hearts Team</p>
</body>
</html>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>[Rusty Hearts] Password Reset Request</title>
</head>
<body>
<p>Hello {{windycode}},</p>
<p>We received a request to reset your Rusty Hearts account password. To proceed with the password reset, please enter the verification code below:</p>
<div style="background-color: #f2f2f2; border: 1px solid #000; padding: 10px;">
{{verificationCode}}
</div>
<p>This code will expire in 10 minutes for security reasons. If you do not enter the code within this timeframe, you may need to request a new one.</p>
<p>If you did not initiate this password reset or do not recognize this email, please disregard it and contact our support team immediately to protect your account.</p>
<p>Thank you for playing Rusty Hearts!</p>
<p>Best regards,<br>Rusty Hearts Team</p>
</body>
</html>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>[Rusty Hearts] Account Creation</title>
</head>
<body>
<p>Hello,</p>
<p>Thank you for registering your email address with your Rusty Hearts account. To complete the registration process, please enter the verification code below:</p>
<div style="background-color: #f2f2f2; border: 1px solid #000; padding: 10px;">
{{verificationCode}}
</div>
<p>This code will expire in 10 minutes for security reasons. If you do not enter the code within this timeframe, you may need to request a new one.</p>
<p>If you did not initiate this registration or do not recognize this email, please disregard it and contact our support team immediately.</p>
<p>Thank you for playing Rusty Hearts!</p>
<p>Best regards,<br>Rusty Hearts Team</p>
</body>
</html>

77
src/routes/auth.js Normal file
View file

@ -0,0 +1,77 @@
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');
// Set up database connection
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];
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;

122
src/routes/billing.js Normal file
View file

@ -0,0 +1,122 @@
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');;
// Set up database connection
const { connAccount } = require('../utils/dbConfig');
// Route for handling billing requests
router.post('/', bodyParser.text({
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];
billingLogger.info(`[Billing] Received [${name}] from user [${userid}]`);
// Create a connection pool for the database
const pool = await connAccount;
switch (name) {
case 'currency-request':
const {
recordset
} = await pool
.request()
.input('UserId', sql.VarChar(50), userid)
.input('ServerId', sql.VarChar(50), server)
.execute('GetCurrency');
const row = recordset[0];
if (row && row.Result === 'Success') {
const response = `<result><balance>${row.Zen}</balance></result>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
res.send(response);
} else {
res.send('<status>failed</status>');
return res.status(400).send(row.Result);
}
break;
case 'item-purchase-request':
const charid = request.charid[0];
const uniqueid = request.uniqueid[0];
const amount = request.amount[0];
const itemid = request.itemid[0];
const itemcount = request.count[0];
const {
recordset: currencyRecordset
} = await pool
.request()
.input('UserId', sql.VarChar(50), userid)
.input('ServerId', sql.VarChar(50), server)
.execute('GetCurrency');
const currencyRow = currencyRecordset[0];
if (currencyRow && currencyRow.Result === 'Success') {
const balance = currencyRow.Zen;
if (amount > 0) {
if (amount > balance) {
res.send('<status>failed</status>');
billingLogger.info(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] failed. Not enough Zen [${balance}]. charid: [${charid}] itemid: [${itemid}] itemcount: [${itemcount}] price: [${amount}]`);
} else {
const newbalance = balance - amount;
await pool
.request()
.input('UserId', sql.VarChar(50), userid)
.input('ServerId', sql.VarChar(50), server)
.input('NewBalance', sql.BigInt, newbalance)
.execute('SetCurrency');
await pool
.request()
.input('userid', sql.VarChar(50), userid)
.input('charid', sql.VarChar(50), charid)
.input('uniqueid', sql.VarChar(50), uniqueid)
.input('amount', sql.BigInt, amount)
.input('itemid', sql.VarChar(50), itemid)
.input('itemcount', sql.Int, itemcount)
.execute('SetBillingLog');
billingLogger.info(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] success. charid: [${charid}] itemid: [${itemid}] itemcount: [${itemcount}] price: [${amount}]`);
billingLogger.info(`[Billing] Item purchase from user [${userid}] success. New zen balance: [${newbalance}]`);
const response = `<result><status>success</status><new-balance>${newbalance}</new-balance></result>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
res.send(response);
}
} else {
const response = `<result><balance>${currencyRow.Zen}</balance></result>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
res.send(response);
}
} else {
res.send('<status>failed</status>');
}
break;
default:
res.send('<status>failed</status>');
break;
}
} catch (error) {
billingLogger.error(`[Billing] Error handling request: $ {
error.message
}`);
res.status(500).send('<status>failed</status>');
}
});
module.exports = router;

54
src/routes/gateway.js Normal file
View file

@ -0,0 +1,54 @@
const express = require('express');
const router = express.Router();
const net = require('net');
// Define the gateway route
router.get('/', (req, res) => {
const ip = process.env.GATESERVER_IP;
const port = process.env.GATESERVER_PORT || '50001';
// Generate the XML content with the IP and port values
const xml = `<?xml version="1.0" encoding="ISO-8859-1"?>
<network>
<gateserver ip="${ip}" port="${port}" />
</network>`;
res.set('Content-Type', 'application/xml');
res.send(xml);
});
// Define the gateway info route
router.get('/info', (req, res) => {
const gatewayRoute = `1|${req.protocol}://${req.headers.host}/serverApi/gateway|${req.protocol}://${req.headers.host}/serverApi/gateway|`;
res.send(gatewayRoute);
});
// Define the gateway status route
router.get('/status', async (req, res) => {
const ip = process.env.GATESERVER_IP;
const port = process.env.GATESERVER_PORT || '50001';
const timeout = 2000;
// Create a new socket and connect to the gateserver
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.connect(port, ip);
// Handle the socket events to check the connection status
socket.on('connect', () => {
res.status(200).json({ status: 'online' });
socket.destroy();
});
socket.on('timeout', () => {
res.status(408).json({ status: 'offline' });
socket.destroy();
});
socket.on('error', () => {
res.status(503).json({ status: 'offline' });
socket.destroy();
});
});
module.exports = router;

View file

@ -0,0 +1,108 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const { logger, accountLogger } = require('../../utils/logger');
const { sendPasswordChangedEmail } = require('../../mailer/mailer');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required(),
verification_code: Joi.string().pattern(new RegExp('^[0-9]+$')).required()
});
// Route for registering an account
router.post('/', async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const email = value.email;
const password = value.password;
const verificationCode = value.verification_code;
// Use a prepared statement to get the verification code
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
request.input('VerificationCode', sql.VarChar, verificationCode);
request.input('VerificationCodeType', sql.VarChar, 'Password');
const inputResult = await request.execute('GetVerificationCode');
const inputRow = inputResult.recordset[0];
if (inputRow && inputRow.Result === 'ValidVerificationCode') {
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, email);
const getResult = await request.execute('GetAccount');
const getRow = getResult.recordset[0];
if (getRow && getRow.Result === 'AccountExists') {
const windyCode = getRow.WindyCode
const hash = getRow.AccountPwd;
// Verify the password
const md5_password = crypto
.createHash('md5')
.update(windyCode + password)
.digest('hex');
const password_verify_result = await bcrypt.compare(
md5_password,
hash
);
if (password_verify_result === true) {
return res.status(400).send('SamePassword');
} else {
const passwordHash = await bcrypt.hash(md5_password, 10);
// Use a prepared statement to update the password
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
request.input('AccountPwd', sql.VarChar, passwordHash);
const updateResult = await request.execute('UpdateAccountPassword');
const updateRow = updateResult.recordset[0];
if (updateRow && updateRow.Result === 'PasswordChanged') {
accountLogger.info(`[Account] Password for [${windyCode}] changed successfully`);
sendPasswordChangedEmail(email, windyCode);
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
const clearResult = await request.execute('ClearVerificationCode');
const clearRow = clearResult.recordset[0];
return res.status(200).send('PasswordChanged');
} else {
accountLogger.info(`[Account] Password change for [${windyCode}] failed: ${row.Result}`);
return res.status(400).send(updateRow.Result);
}
}
} else {
return res.status(400).send(getRow.Result);
}
} else {
return res.status(400).send(inputRow.Result);
}
} catch (error) {
logger.error('[Account] Database query failed: ' + error.message);
return res.status(500).send('[Account] Database query failed: ' + error.message);
}
});
module.exports = router;

View file

@ -0,0 +1,53 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const { logger } = require('../../utils/logger');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required(),
verification_code_type: Joi.string().required(),
verification_code: Joi.string().pattern(new RegExp('^[0-9]+$')).required()
});
// Route for registering an account
router.post('/', async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const email = req.body.email;
const verificationCode = req.body.verification_code;
const verificationCodeType = req.body.verification_code_type;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).send('InvalidEmailFormat');
}
if (!/^\d+$/.test(verificationCode)) {
return res.status(400).send('InvalidVerificationCodeFormat');
}
// Use a prepared statement to check verification code
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
request.input('VerificationCode', sql.VarChar, verificationCode);
request.input('VerificationCodeType', sql.VarChar, verificationCodeType);
const result = await request.execute('GetVerificationCode');
const row = result.recordset[0];
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);
}
});
module.exports = router;

View file

@ -0,0 +1,41 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const { logger } = require('../../utils/logger');
const router = express.Router();
// Endpoint to get the launcher version from the launcher_info.ini file
router.get('/getLauncherVersion', (req, res) => {
const launcherInfoPath = path.join(__dirname, '..', '..', '..', 'public', 'launcher', 'launcher_update', 'launcher_info.ini');
fs.readFile(launcherInfoPath, 'utf8', (err, data) => {
if (err) {
console.error(err);
return res.status(500).send('Error reading launcher_info.ini');
}
const versionRegex = /version=(.*)/i;
const match = data.match(versionRegex);
if (match) {
const launcherVersion = match[1];
return res.json({ version: launcherVersion });
}
return res.status(500).send('Invalid launcher_info.ini format');
});
});
// Endpoint to download the new launcher version from the launcher_update folder
router.post('/updateLauncherVersion', (req, res) => {
const launcherUpdatePath = path.join(__dirname, '..', '..', '..', 'public', 'launcher', 'launcher_update');
const version = req.body.version;
if (!req.body.version) {
return res.status(400).send('Missing version parameter');
}
const file = path.join(launcherUpdatePath, `launcher_${version}.zip`);
if (!fs.existsSync(file)) {
return res.status(404).send(`File ${file} not found`);
logger.error(`[Launcher Updater] File ${file} not found`);
}
res.download(file);
});
module.exports = router;

View file

@ -0,0 +1,100 @@
const sql = require('mssql');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const express = require('express');
const router = express.Router();
const { logger, accountLogger } = require('../../utils/logger');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Define the validation schema for the request body
const schema = Joi.object({
account: Joi.string().required(),
password: Joi.string().required(),
});
router.post('/', async (req, res) => {
try {
// Validate the request body against the schema
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const account = value.account;
const password = value.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' });
}
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, account);
const result = await request.execute('GetAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountExists') {
const windyCode = row.WindyCode;
const hash = row.AccountPwd;
// Verify the password
const md5_password = crypto
.createHash('md5')
.update(windyCode + password)
.digest('hex');
const password_verify_result = await bcrypt.compare(
md5_password,
hash
);
const authRequest = pool.request();
authRequest.input('Identifier', sql.VarChar, account);
authRequest.input(
'password_verify_result',
sql.Bit,
password_verify_result
);
authRequest.input('LastLoginIP', sql.VarChar, userIp);
const authResult = await authRequest.execute('AuthenticateUser');
const authRow = authResult.recordset[0];
if (authRow && authRow.Result === 'LoginSuccess') {
accountLogger.info(
`[Account] Launcher Login: Account [${windyCode}] successfully logged in from [${userIp}]`
);
return res.status(200).json({
Result: authRow.Result,
Token: authRow.Token,
WindyCode: authRow.WindyCode,
});
} else {
accountLogger.info(
`[Account] Launcher Login: Account [${windyCode}] login failed: ${authRow.Result} `
);
return res.status(400).json({
Result: authRow.Result,
});
}
} else {
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);
}
});
module.exports = router;

View file

@ -0,0 +1,74 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const { logger, accountLogger } = require('../../utils/logger');
const { sendPasswordResetEmail } = require('../../mailer/mailer');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required()
});
// Route for registering an account
router.post('/', async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const email = req.body.email;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
logger.info('Invalid email format');
return res.status(400).send('InvalidEmailFormat');
}
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, email);
const result = await request.execute('GetAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountExists') {
const emailAdress = row.Email;
const windycode = row.WindyCode;
const verificationCode = Math.floor(10000 + Math.random() * 90000).toString();
const expirationTime = new Date(Date.now() + 600000).toISOString(); // 10 minutes from now
// Prepare the second statement to insert the verification code information
const insertRequest = pool.request();
insertRequest.input('Email', sql.VarChar, email);
insertRequest.input('VerificationCode', sql.VarChar, verificationCode);
insertRequest.input('ExpirationTime', sql.DateTime, expirationTime);
const insertResult = await insertRequest.execute('SetPasswordVerificationCode');
const insertRow = insertResult.recordset[0];
if (insertRow && insertRow.Result === 'Success') {
// Send verification code email
sendPasswordResetEmail(email, verificationCode);
return res.status(200).send('EmailSent');
}
else {
accountLogger.info(`[Account] Failed to insert verification code for email: ${email}`);
return res.status(500).send(insertRow.Result);
}
} else if (row && row.Result === 'AccountNotFound') {
return res.status(400).send('AccountNotFound');
} else {
return res.status(500).send(row.Result);
}
} catch (error) {
logger.error('[Account] Database query failed: ' + error.message);
return res.status(500).send('Database query failed: ' + error.message);
}
});
module.exports = router;

View file

@ -0,0 +1,67 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const { logger, accountLogger } = require('../../utils/logger');
const { sendConfirmationEmail } = require('../../mailer/mailer');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for validating request data
const schema = Joi.object({
windyCode: Joi.string().alphanum().min(1).max(16).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
// Route for registering an account
router.post('/', async (req, res) => {
try {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const windyCode = value.windyCode;
const email = value.email;
const password = value.password;
const userIp = req.ip;
const md5_password = crypto.createHash('md5').update(windyCode + password).digest('hex'); // Generate MD5 hash
const passwordHash = await bcrypt.hash(md5_password, 10);
// Use a prepared statement to create the account
const pool = await connAccount;
const request = pool.request();
request.input('WindyCode', sql.VarChar, windyCode);
request.input('AccountPwd', sql.VarChar, passwordHash);
request.input('Email', sql.VarChar, email);
request.input('RegisterIP', sql.VarChar, userIp);
const result = await request.execute('CreateAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountCreated') {
accountLogger.info(`[Account] Account [${windyCode}] created successfully`);
sendConfirmationEmail(email, windyCode);
const clearRequest = pool.request();
clearRequest.input('Email', sql.VarChar, email);
const clearResult = await clearRequest.execute('ClearVerificationCode');
const clearRow = clearResult.recordset[0];
return res.status(200).send('Success');
} else {
accountLogger.info(`[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);
}
});
module.exports = router;

View file

@ -0,0 +1,68 @@
// Load environment variables
const env = require('../../utils/env');
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const { logger } = require('../../utils/logger');
const { sendVerificationEmail } = require('../../mailer/mailer');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required()
});
// Route for registering an account
router.post('/', async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const email = req.body.email;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).send('InvalidEmailFormat');
}
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, email);
const result = await request.execute('GetAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountNotFound') {
const verificationCode = Math.floor(10000 + Math.random() * 90000).toString();
const timeZone = process.env.TZ;
// Set the expiration time 10 minutes from now in the specified timezone
const expirationTime = new Date(Date.now() + 600000).toLocaleString('en-US', { timeZone });
// Prepare the second statement to insert the verification code information
const insertRequest = pool.request();
insertRequest.input('Email', sql.VarChar, email);
insertRequest.input('VerificationCode', sql.VarChar, verificationCode);
insertRequest.input('ExpirationTime', sql.DateTime, expirationTime);
const insertResult = await insertRequest.execute('SetAccountVerificationCode');
const insertRow = insertResult.recordset[0];
// Send verification code email
sendVerificationEmail(email, verificationCode);
return res.status(200).send('EmailSent');
} else {
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);
}
});
module.exports = router;

21
src/utils/dbConfig.js Normal file
View file

@ -0,0 +1,21 @@
const sql = require('mssql');
const env = require('./env');
const { logger } = require('../utils/logger');
const dbConfig = {
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
server: process.env.DB_SERVER,
database: process.env.DB_DATABASE,
options: {
encrypt: process.env.DB_ENCRYPT === 'true'
}
};
const connAccount = new sql.ConnectionPool(dbConfig);
connAccount.connect();
logger.info(`Database: Connected.`);
module.exports = {
connAccount
};

6
src/utils/env.js Normal file
View file

@ -0,0 +1,6 @@
require('dotenv').config();
// Access environment variables with a function
module.exports = {
get: (key) => process.env[key],
};

144
src/utils/logger.js Normal file
View file

@ -0,0 +1,144 @@
const fs = require("fs");
const path = require("path");
const util = require("util");
const winston = require("winston");
const logsDirectory = 'logs';
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: [
new winston.transports.File({
filename: `logs/auth-${new Date().toISOString().slice(0, 10)}.log`,
level: 'info',
filter: (log) => log.message.includes('[Auth]')
})
]
});
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({
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'
})
]
});
module.exports = {
authLogger,
billingLogger,
mailerLogger,
accountLogger,
logger
};