mirror of
https://github.com/JuniorDark/RustyHearts-API.git
synced 2026-05-07 06:01:44 -04:00
Version 1.3.0
- Implemented authentication and billing routes for Jpn region. - Refactored and changed the project structure from CommonJS to ES Modules
This commit is contained in:
parent
9584e58143
commit
c3d9e7afb5
76 changed files with 3847 additions and 1109 deletions
187
src/app.js
187
src/app.js
|
|
@ -1,163 +1,48 @@
|
|||
// Load environment variables
|
||||
const env = require('./utils/env');
|
||||
import config from './config.js';
|
||||
const { logger } = config;
|
||||
import { logSystemInfo } from './utils/systemInfo.js';
|
||||
import memoryLogger from './utils/memoryLogger.js';
|
||||
const { setupMemoryLogging } = memoryLogger;
|
||||
|
||||
// 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 moment = require('moment-timezone');
|
||||
const { logger } = require('./utils/logger');
|
||||
const path = require('path');
|
||||
// Import servers
|
||||
import { startServer as startMainApp } from './servers/mainApp.js';
|
||||
import { startServer as startUsaApp } from './servers/usaApp.js';
|
||||
import { startServer as startJpnApp } from './servers/jpnApp.js';
|
||||
import { startServer as startProxyServer } from './servers/proxyServer.js';
|
||||
|
||||
// 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');
|
||||
const onlineCountRouter = require('./routes/onlineCount');
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const serversToStart = args.length > 0 ? args : ['mainApp'];
|
||||
|
||||
// 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'
|
||||
});
|
||||
// Start selected servers
|
||||
const activeServers = [];
|
||||
|
||||
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());
|
||||
if (serversToStart.includes('mainApp')) {
|
||||
activeServers.push(startMainApp());
|
||||
}
|
||||
|
||||
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);
|
||||
app.use('/serverApi/onlineCount', limiter , onlineCountRouter);
|
||||
|
||||
// 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')));
|
||||
app.use('/launcher/client', express.static(path.join(__dirname, '../public/launcher/client')));
|
||||
|
||||
|
||||
// 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('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]}`;
|
||||
if (serversToStart.includes('usaApp')) {
|
||||
activeServers.push(startUsaApp());
|
||||
}
|
||||
|
||||
// Start server
|
||||
const port = process.env.PORT || 3000;
|
||||
const publicIP = process.env.PUBLIC_IP || '0.0.0.0';
|
||||
|
||||
console.log('--------------------------------------------------');
|
||||
console.log(`Rusty Hearts API Version: 1.2`)
|
||||
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('--------------------------------------------------');
|
||||
|
||||
// Function to log memory usage
|
||||
function logMemoryUsage() {
|
||||
const now = new Date();
|
||||
const formattedDateTime = moment(now).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
const memoryUsage = process.memoryUsage();
|
||||
|
||||
console.log(`Memory Usage at ${formattedDateTime}:`);
|
||||
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('--------------------------------------------------');
|
||||
if (serversToStart.includes('jpnApp')) {
|
||||
activeServers.push(startJpnApp());
|
||||
}
|
||||
|
||||
// Log memory usage every 30 minutes (1800000 milliseconds)
|
||||
const memoryLogInterval = 1800000;
|
||||
if (serversToStart.includes('proxyServer')) {
|
||||
activeServers.push(startProxyServer());
|
||||
}
|
||||
|
||||
setInterval(logMemoryUsage, memoryLogInterval);
|
||||
// System Info
|
||||
logSystemInfo();
|
||||
|
||||
app.listen(port, publicIP, () => {
|
||||
logger.info(`API listening on ${publicIP}:${port}`);
|
||||
logger.info(`Auth API listening on 127.0.0.1:${authPort}`);
|
||||
logger.info(`Billing API listening on 127.0.0.1:${billingPort}`);
|
||||
});
|
||||
// Memory Logging
|
||||
const stopMemoryLogging = setupMemoryLogging();
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('Shutting down servers...');
|
||||
activeServers.forEach(server => server.close());
|
||||
stopMemoryLogging();
|
||||
process.exit();
|
||||
});
|
||||
100
src/config.js
Normal file
100
src/config.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
import cors from 'cors';
|
||||
import compression from 'compression';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
|
||||
import { logger } from './utils/logger.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export default {
|
||||
// Port configurations
|
||||
ports: {
|
||||
main: process.env.API_LISTEN_PORT || 80,
|
||||
usaApp: process.env.API_USA_PORT || 8070,
|
||||
jpnApp: process.env.API_JPN_PORT || 8080,
|
||||
proxy: process.env.API_PROXY_PORT || 8090,
|
||||
gate: process.env.GATESERVER_PORT || 50001,
|
||||
},
|
||||
|
||||
// IP configurations
|
||||
ips: {
|
||||
public: process.env.API_LISTEN_HOST || '0.0.0.0',
|
||||
local: process.env.API_LOCAL_LISTEN_HOST || '127.0.0.1',
|
||||
gate: process.env.GATESERVER_IP,
|
||||
},
|
||||
|
||||
// configurations
|
||||
config: {
|
||||
serverId: Number(process.env.SERVER_ID) || 10101,
|
||||
shopBalance: process.env.API_SHOP_INITIAL_BALANCE || 0,
|
||||
timeZone: process.env.TZ,
|
||||
},
|
||||
|
||||
// API configurations
|
||||
apiConfig: {
|
||||
trustProxyEnabled: process.env.API_TRUSTPROXY_ENABLE || 'false',
|
||||
trustProxyHosts: process.env.API_TRUSTPROXY_HOSTS || [],
|
||||
logIPAddresses: process.env.LOG_IP_ADDRESSES || 'false',
|
||||
},
|
||||
|
||||
// Mailer configurations
|
||||
mailer: {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(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
|
||||
}
|
||||
},
|
||||
|
||||
// middleware
|
||||
middleware: {
|
||||
baseMiddleware: [
|
||||
cors(),
|
||||
compression(),
|
||||
express.json(),
|
||||
express.urlencoded({ extended: true }),
|
||||
],
|
||||
|
||||
getMiddleware: function () {
|
||||
const middleware = [...this.baseMiddleware];
|
||||
if (process.env.API_ENABLE_HELMET === 'true') {
|
||||
middleware.unshift(helmet());
|
||||
}
|
||||
return middleware;
|
||||
}
|
||||
},
|
||||
|
||||
// Static file paths
|
||||
staticPaths: {
|
||||
public: path.join(__dirname, '../public'),
|
||||
launcherNews: path.join(__dirname, '../public/launcher/news'),
|
||||
launcherNewsImages: path.join(__dirname, '../public/launcher/news/images'),
|
||||
launcherPatch: path.join(__dirname, '../public/launcher/patch'),
|
||||
launcherClient: path.join(__dirname, '../public/launcher/client'),
|
||||
site: path.join(__dirname, '../public/site'),
|
||||
},
|
||||
|
||||
// Logger
|
||||
logger,
|
||||
|
||||
// Backend configuration for proxy
|
||||
BACKENDS: {
|
||||
AUTH: {
|
||||
paths: ['/Auth/cgi-bin/auth_rest_oem.cgi']
|
||||
},
|
||||
BILLING: {
|
||||
paths: ['/Billing/S1/ApiPointTotalGetS.php', '/Billing/S1/ApiPointMoveS.php']
|
||||
}
|
||||
}
|
||||
};
|
||||
41
src/config/rateLimitConfig.js
Normal file
41
src/config/rateLimitConfig.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
||||
import { api } from '../../config/rateLimits.default.js';
|
||||
|
||||
class RateLimitConfig {
|
||||
constructor() {
|
||||
this.limiters = new Map();
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
// API Limiters for /launcher
|
||||
for (const [endpoint, config] of Object.entries(api.launcher)) {
|
||||
this.limiters.set(
|
||||
`/launcher/${endpoint.charAt(0).toUpperCase()}${endpoint.slice(1)}`,
|
||||
new RateLimiterMemory({
|
||||
points: config.points,
|
||||
duration: config.duration,
|
||||
blockDuration: config.blockDuration
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// API Limiters for /launcherAction
|
||||
for (const [endpoint, config] of Object.entries(api.launcherAction)) {
|
||||
this.limiters.set(
|
||||
`/launcherAction/${endpoint}`,
|
||||
new RateLimiterMemory({
|
||||
points: config.points,
|
||||
duration: config.duration,
|
||||
blockDuration: config.blockDuration
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getLimiter(path) {
|
||||
return this.limiters.get(path);
|
||||
}
|
||||
}
|
||||
|
||||
export default new RateLimitConfig();
|
||||
7
src/lib/closeConnection.js
Normal file
7
src/lib/closeConnection.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// This middleware sets the "Connection" header to "close" for all responses.
|
||||
// It ensures that the connection is closed after the response is sent, which can help with resource management and performance.
|
||||
// The 'next()' function is called to pass control to the next middleware or route handler in the stack.
|
||||
export const closeConnection = (req, res, next) => {
|
||||
res.set("Connection", "close");
|
||||
next();
|
||||
};
|
||||
43
src/lib/rateLimiter.js
Normal file
43
src/lib/rateLimiter.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import rateLimitConfig from "../config/rateLimitConfig.js";
|
||||
import { getClientIp } from "../utils/getClientIp.js";
|
||||
|
||||
async function rateLimiter(req, res, next) {
|
||||
try {
|
||||
const path = (req.baseUrl + req.path).replace(/\/+$/, "");
|
||||
const limiterKey = Array.from(rateLimitConfig.limiters.keys()).find((key) =>
|
||||
path.startsWith(key)
|
||||
);
|
||||
|
||||
if (!limiterKey) {
|
||||
console.warn(`[RateLimiter] No limiter found for path: "${path}"`);
|
||||
return next();
|
||||
}
|
||||
|
||||
const limiter = rateLimitConfig.getLimiter(limiterKey);
|
||||
|
||||
const clientIP = getClientIp(req);
|
||||
const rateLimitRes = await limiter.consume(clientIP);
|
||||
|
||||
res.set({
|
||||
"Retry-After": rateLimitRes.msBeforeNext / 1000,
|
||||
"X-RateLimit-Limit": limiter.points,
|
||||
"X-RateLimit-Remaining": rateLimitRes.remainingPoints,
|
||||
"X-RateLimit-Reset": new Date(Date.now() + rateLimitRes.msBeforeNext),
|
||||
});
|
||||
|
||||
next();
|
||||
} catch (rateLimitRes) {
|
||||
res.set({
|
||||
"Retry-After": rateLimitRes.msBeforeNext / 1000,
|
||||
});
|
||||
|
||||
return res.status(429).json({
|
||||
error: "Too many requests",
|
||||
message: `You've exceeded the rate limit. Please try again in ${Math.ceil(
|
||||
rateLimitRes.msBeforeNext / 1000
|
||||
)} seconds.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default rateLimiter;
|
||||
|
|
@ -1,18 +1,24 @@
|
|||
const nodemailer = require('nodemailer');
|
||||
const handlebars = require('handlebars');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { mailerLogger } = require('../utils/logger');
|
||||
import { createTransport } from 'nodemailer';
|
||||
import handlebars from 'handlebars';
|
||||
import { readFileSync } from 'fs';
|
||||
import { logger, mailerLogger } from '../utils/logger.js';
|
||||
import path from 'path';
|
||||
import config from '../config.js';
|
||||
const { mailer } = config;
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
// Load the email templates
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load and compile 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')
|
||||
confirmation: readFileSync(path.join(__dirname, 'templates', 'confirmationTemplate.hbs'), 'utf-8'),
|
||||
verification: readFileSync(path.join(__dirname, 'templates', 'verificationTemplate.hbs'), 'utf-8'),
|
||||
passwordReset: readFileSync(path.join(__dirname, 'templates', 'passwordResetTemplate.hbs'), 'utf-8'),
|
||||
passwordChanged: 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),
|
||||
|
|
@ -20,23 +26,47 @@ const compiledTemplates = {
|
|||
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
|
||||
}
|
||||
});
|
||||
// Check for required environment variables
|
||||
function isMailerConfigured() {
|
||||
const requiredEnvVars = [
|
||||
'SMTP_HOST',
|
||||
'SMTP_PORT',
|
||||
'SMTP_ENCRYPTION',
|
||||
'SMTP_USERNAME',
|
||||
'SMTP_PASSWORD',
|
||||
'SMTP_FROM_NAME'
|
||||
];
|
||||
|
||||
const missing = requiredEnvVars.filter(key => !process.env[key]);
|
||||
if (missing.length) {
|
||||
logger.warn(`[Mailer] SMTP server is not configured. Missing environment variables: ${missing.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create transporter
|
||||
function getTransporter() {
|
||||
return createTransport({
|
||||
host: mailer.host,
|
||||
port: Number(mailer.port),
|
||||
secure: mailer.secure,
|
||||
auth: {
|
||||
user: mailer.auth.user,
|
||||
pass: mailer.auth.pass
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Email send functions
|
||||
function sendConfirmationEmail(email, windyCode) {
|
||||
const template = compiledTemplates.confirmation;
|
||||
const emailContent = template({ windyCode });
|
||||
if (!isMailerConfigured()) return;
|
||||
|
||||
const transporter = getTransporter();
|
||||
const emailContent = compiledTemplates.confirmation({ windyCode });
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
|
||||
to: email,
|
||||
subject: '[Rusty Hearts] Account Creation Confirmation',
|
||||
html: emailContent
|
||||
|
|
@ -44,19 +74,21 @@ function sendConfirmationEmail(email, windyCode) {
|
|||
|
||||
transporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
mailerLogger.error('[Mailer] Error sending confirmation email: ' + error.message);
|
||||
mailerLogger.error('[Mailer] Error sending confirmation email: ' + error.message);
|
||||
} else {
|
||||
mailerLogger.info('[Mailer] Confirmation email sent: ' + info.response);
|
||||
mailerLogger.info('[Mailer] Confirmation email sent: ' + info.response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendVerificationEmail(email, verificationCode) {
|
||||
const template = compiledTemplates.verification;
|
||||
const emailContent = template({ verificationCode });
|
||||
if (!isMailerConfigured()) return;
|
||||
|
||||
const transporter = getTransporter();
|
||||
const emailContent = compiledTemplates.verification({ verificationCode });
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
|
||||
to: email,
|
||||
subject: '[Rusty Hearts] Account Creation',
|
||||
html: emailContent
|
||||
|
|
@ -72,11 +104,13 @@ function sendVerificationEmail(email, verificationCode) {
|
|||
}
|
||||
|
||||
function sendPasswordResetEmail(email, verificationCode) {
|
||||
const template = compiledTemplates.passwordReset;
|
||||
const emailContent = template({ verificationCode });
|
||||
if (!isMailerConfigured()) return;
|
||||
|
||||
const transporter = getTransporter();
|
||||
const emailContent = compiledTemplates.passwordReset({ verificationCode });
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
|
||||
to: email,
|
||||
subject: '[Rusty Hearts] Password Reset Request',
|
||||
html: emailContent
|
||||
|
|
@ -92,11 +126,13 @@ function sendPasswordResetEmail(email, verificationCode) {
|
|||
}
|
||||
|
||||
function sendPasswordChangedEmail(email, windyCode) {
|
||||
const template = compiledTemplates.passwordChanged;
|
||||
const emailContent = template({ windyCode });
|
||||
if (!isMailerConfigured()) return;
|
||||
|
||||
const transporter = getTransporter();
|
||||
const emailContent = compiledTemplates.passwordChanged({ windyCode });
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
|
||||
to: email,
|
||||
subject: '[Rusty Hearts] Account Password Changed',
|
||||
html: emailContent
|
||||
|
|
@ -111,4 +147,9 @@ function sendPasswordChangedEmail(email, windyCode) {
|
|||
});
|
||||
}
|
||||
|
||||
module.exports = {sendConfirmationEmail, sendVerificationEmail, sendPasswordResetEmail, sendPasswordChangedEmail};
|
||||
export {
|
||||
sendConfirmationEmail,
|
||||
sendVerificationEmail,
|
||||
sendPasswordResetEmail,
|
||||
sendPasswordChangedEmail
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const xml2js = require('xml2js');
|
||||
const sql = require('mssql');
|
||||
const Joi = require('joi');
|
||||
|
||||
const { authLogger } = require('../utils/logger');
|
||||
const { connAccount } = require('../utils/dbConfig');
|
||||
|
||||
const router = express.Router();
|
||||
const parser = new xml2js.Parser({ explicitArray: false });
|
||||
|
||||
// Joi schema for request body validation
|
||||
const schema = Joi.object({
|
||||
'login-request': Joi.object({
|
||||
account: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
game: Joi.string().required(),
|
||||
ip: Joi.string().required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
// Route for handling login requests
|
||||
router.post('/', 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;
|
||||
114
src/routes/authJpn.js
Normal file
114
src/routes/authJpn.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { Router, urlencoded } from "express";
|
||||
import joi from "joi";
|
||||
import config from "../config.js";
|
||||
const { apiConfig } = config;
|
||||
import { authLogger } from "../utils/logger.js";
|
||||
import { authenticateUser } from "../services/accountDBService.js";
|
||||
import { getClientIp } from "../utils/getClientIp.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const schema = joi.object({
|
||||
service_id: joi.string().required(),
|
||||
product_name: joi.string().allow("").optional(),
|
||||
original_id: joi.string().required(),
|
||||
original_password: joi.string().required(),
|
||||
ip: joi.string().ip().required(),
|
||||
});
|
||||
|
||||
function buildSuccessResponse(statusCode, idFlg, userId, authToken) {
|
||||
return `<response>
|
||||
<status>${statusCode}</status>
|
||||
<id_flg>${idFlg}</id_flg>
|
||||
<user_id>${userId}</user_id>
|
||||
<auth_token>${authToken}</auth_token>
|
||||
</response>`;
|
||||
}
|
||||
|
||||
function buildErrorResponse(statusCode, idFlg) {
|
||||
return `<response>
|
||||
<status>${statusCode}</status>
|
||||
<id_flg>${idFlg}</id_flg>
|
||||
</response>`;
|
||||
}
|
||||
|
||||
router.post(
|
||||
"/cgi-bin/auth_rest_oem.cgi",
|
||||
urlencoded({ extended: true }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
res.set({
|
||||
"Content-Type": "text/xml",
|
||||
Connection: "close",
|
||||
});
|
||||
|
||||
const ip = getClientIp(req);
|
||||
|
||||
// Validate request
|
||||
const { error } = schema.validate({ ...req.body, ip });
|
||||
if (error) {
|
||||
authLogger.warn(`[Auth] [JPN] Invalid request: ${error.message}`);
|
||||
return res.send(buildErrorResponse(1, 0));
|
||||
}
|
||||
|
||||
const { original_id, original_password } = req.body;
|
||||
|
||||
authLogger.info(
|
||||
apiConfig.logIPAddresses === "true"
|
||||
? `[Auth] [JPN] Account [${original_id}] is trying to login from [${ip}]`
|
||||
: `[Auth] [JPN] Account [${original_id}] is trying to login`
|
||||
);
|
||||
|
||||
// Authenticate user
|
||||
const authResult = await authenticateUser(
|
||||
original_id,
|
||||
original_password,
|
||||
ip
|
||||
);
|
||||
|
||||
// Handle different authentication results
|
||||
switch (authResult.status) {
|
||||
case "LoginSuccess":
|
||||
authLogger.info(
|
||||
`[Auth] [JPN] Account [${original_id}] login success`
|
||||
);
|
||||
return res.send(
|
||||
buildSuccessResponse(0, 0, authResult.authId, authResult.token)
|
||||
);
|
||||
|
||||
case "Locked":
|
||||
authLogger.warn(
|
||||
`[Auth] [JPN] Account [${original_id}] login failed - account locked`
|
||||
);
|
||||
return res.send(buildErrorResponse(0, 2));
|
||||
|
||||
case "InvalidCredentials":
|
||||
authLogger.warn(
|
||||
`[Auth] [JPN] Account [${original_id}] login failed - invalid credentials`
|
||||
);
|
||||
return res.send(buildErrorResponse(1, 0));
|
||||
|
||||
case "TooManyAttempts":
|
||||
authLogger.warn(
|
||||
`[Auth] [JPN] Account [${original_id}] login failed - too many attempts`
|
||||
);
|
||||
return res.send(buildErrorResponse(8, 0));
|
||||
|
||||
case "AccountNotFound":
|
||||
authLogger.warn(`[Auth] [JPN] Account [${original_id}] not found`);
|
||||
return res.send(buildErrorResponse(2, 0));
|
||||
|
||||
default:
|
||||
authLogger.error(
|
||||
`[Auth] [JPN] Unknown authentication status: ${authResult.status}`
|
||||
);
|
||||
return res.send(buildErrorResponse(18, 0));
|
||||
}
|
||||
} catch (error) {
|
||||
authLogger.error(`[Auth] [JPN] Error handling auth request: ${error.message}`);
|
||||
return res.send(buildErrorResponse(18, 0));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
110
src/routes/authUsa.js
Normal file
110
src/routes/authUsa.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { Router, text } from "express";
|
||||
import { Parser } from "xml2js";
|
||||
import joi from "joi";
|
||||
import config from "../config.js";
|
||||
const { apiConfig } = config;
|
||||
import { authLogger } from "../utils/logger.js";
|
||||
import { authenticateUser } from "../services/accountDBService.js";
|
||||
import { getClientIp } from "../utils/getClientIp.js";
|
||||
|
||||
const router = Router();
|
||||
const parser = new Parser({ explicitArray: false });
|
||||
|
||||
// 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(),
|
||||
});
|
||||
|
||||
function buildSuccessResponse(userId, userType, authToken) {
|
||||
return `<userid>${userId}</userid>
|
||||
<user-type>${userType}</user-type>
|
||||
<auth_token>${authToken}</auth_token>
|
||||
<status>success</status>`;
|
||||
}
|
||||
|
||||
function buildErrorResponse(message) {
|
||||
return `<status>failed</status>
|
||||
<message>${message}</message>`;
|
||||
}
|
||||
|
||||
router.post("/", text({ type: "*/xml" }), async (req, res) => {
|
||||
try {
|
||||
res.set({
|
||||
"Content-Type": "text/xml",
|
||||
Connection: "close",
|
||||
});
|
||||
|
||||
const ip = getClientIp(req);
|
||||
const xml = req.body;
|
||||
const result = await parser.parseStringPromise(xml);
|
||||
|
||||
const { error, value } = schema.validate(result);
|
||||
if (error) {
|
||||
authLogger.info(`[Auth] [USA] Invalid login request: ${error.message}`);
|
||||
return res.send(buildErrorResponse("ValidationError"));
|
||||
}
|
||||
|
||||
const { "login-request": loginRequest } = value;
|
||||
const { account, password } = loginRequest;
|
||||
|
||||
authLogger.info(
|
||||
apiConfig.logIPAddresses === "true"
|
||||
? `[Auth] [USA] Account [${account}] is trying to login from [${ip}]`
|
||||
: `[Auth] [USA] Account [${account}] is trying to login`
|
||||
);
|
||||
|
||||
// Authenticate user
|
||||
const authResult = await authenticateUser(account, password, ip, true);
|
||||
|
||||
// Handle results
|
||||
switch (authResult.status) {
|
||||
case "LoginSuccess":
|
||||
authLogger.info(`[Auth] [USA] Account [${account}] login success`);
|
||||
return res.send(
|
||||
buildSuccessResponse(authResult.authId, "F", authResult.token)
|
||||
);
|
||||
|
||||
case "Locked":
|
||||
authLogger.warn(
|
||||
`[Auth] [USA] Account [${account}] login failed - account locked`
|
||||
);
|
||||
return res.send(buildErrorResponse(authResult.status));
|
||||
|
||||
case "InvalidCredentials":
|
||||
authLogger.warn(
|
||||
`[Auth] [USA] Account [${account}] login failed - invalid credentials`
|
||||
);
|
||||
return res.send(buildErrorResponse(authResult.status));
|
||||
|
||||
case "TooManyAttempts":
|
||||
authLogger.warn(
|
||||
`[Auth] [USA] Account [${account}] login failed - too many attempts`
|
||||
);
|
||||
return res.send(buildErrorResponse(authResult.status));
|
||||
|
||||
case "AccountNotFound":
|
||||
authLogger.warn(`[Auth] [USA] Account [${account}] not found`);
|
||||
return res.send(buildErrorResponse(authResult.status));
|
||||
|
||||
default:
|
||||
authLogger.error(
|
||||
`[Auth] [USA] Unknown authentication status: ${authResult.status}`
|
||||
);
|
||||
return res.send(buildErrorResponse(authResult.status));
|
||||
}
|
||||
} catch (error) {
|
||||
authLogger.error(
|
||||
`[Auth] [USA] Error handling auth request: ${error.message}`
|
||||
);
|
||||
return res.send(buildErrorResponse("ServerError"));
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const xml2js = require('xml2js');
|
||||
const sql = require('mssql');
|
||||
const router = express.Router();
|
||||
const { billingLogger } = require('../utils/logger');
|
||||
const Joi = require('joi');
|
||||
|
||||
// Set up database connection
|
||||
const { connAccount } = require('../utils/dbConfig');
|
||||
|
||||
// Define the validation schema for currency requests
|
||||
const currencySchema = Joi.object({
|
||||
userid: Joi.string().required(),
|
||||
server: Joi.string().required(),
|
||||
game: Joi.string().required(),
|
||||
}).required();
|
||||
|
||||
// Define the validation schema for item purchase requests
|
||||
const itemPurchaseSchema = Joi.object({
|
||||
userid: Joi.string().required(),
|
||||
server: Joi.string().required(),
|
||||
charid: Joi.string().required(),
|
||||
game: Joi.number().required(),
|
||||
uniqueid: Joi.string().required(),
|
||||
amount: Joi.number().required(),
|
||||
itemid: Joi.string().required(),
|
||||
count: Joi.number().required(),
|
||||
}).required();
|
||||
|
||||
|
||||
// Route for handling billing requests
|
||||
router.post('/', bodyParser.text({
|
||||
type: '*/xml'
|
||||
}), async (req, res) => {
|
||||
try {
|
||||
const xml = req.body;
|
||||
const result = await xml2js.parseStringPromise(xml, { explicitArray: false });
|
||||
const name = result['currency-request'] ? 'currency-request' : 'item-purchase-request';
|
||||
const request = result[name];
|
||||
|
||||
// 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
|
||||
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;
|
||||
117
src/routes/billingJpn.js
Normal file
117
src/routes/billingJpn.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { Router, urlencoded } from 'express';
|
||||
import joi from 'joi';
|
||||
import { billingLogger } from '../utils/logger.js';
|
||||
import { validateCredentials, getCurrency, setCurrency } from '../services/accountDBService.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function buildSuccessResponse(balance) {
|
||||
return `<response>
|
||||
<status>0</status>
|
||||
<total_point>${balance}</total_point>
|
||||
</response>`;
|
||||
}
|
||||
|
||||
function buildErrorResponse(statusCode, errorMessage) {
|
||||
return `<response>
|
||||
<status>${statusCode}</status>
|
||||
<error>${errorMessage}</error>
|
||||
</response>`;
|
||||
}
|
||||
|
||||
function extractItemId(itemCode) {
|
||||
if (!itemCode || itemCode.length !== 17 || !itemCode.startsWith('rsty')) {
|
||||
throw new Error('Invalid item_code format');
|
||||
}
|
||||
return parseInt(itemCode.substring(4, 14), 10);
|
||||
}
|
||||
|
||||
// Validation Schemas
|
||||
const baseSchema = {
|
||||
product_name: joi.string().allow('').optional(),
|
||||
original_id: joi.string().required(),
|
||||
original_password: joi.string().required(),
|
||||
auto_charge_exec: joi.number().integer().optional()
|
||||
};
|
||||
|
||||
const purchaseSchema = joi.object({
|
||||
...baseSchema,
|
||||
move_point: joi.number().integer().required(),
|
||||
move_kind: joi.string().valid('06').required(),
|
||||
item_code: joi.string().pattern(/^rsty\d{13}$/).required()
|
||||
});
|
||||
|
||||
const currencySchema = joi.object(baseSchema);
|
||||
|
||||
async function handleBalanceRequest(req, res) {
|
||||
try {
|
||||
res.set({
|
||||
'Content-Type': 'text/xml',
|
||||
'Connection': 'close'
|
||||
});
|
||||
|
||||
const { error, value } = currencySchema.validate(req.body);
|
||||
if (error) throw new Error(`Invalid request: ${error.message}`);
|
||||
|
||||
const { original_id, original_password } = value;
|
||||
|
||||
if (!await validateCredentials(original_id, original_password)) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const balance = await getCurrency(original_id);
|
||||
return res.send(buildSuccessResponse(balance));
|
||||
|
||||
} catch (error) {
|
||||
billingLogger.error(`[Billing] [JPN] Balance request error: ${error.message}`);
|
||||
return res.send(buildErrorResponse(1, error.message.includes('Invalid') ? error.message : 'Invalid request'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePurchaseRequest(req, res) {
|
||||
try {
|
||||
res.set({
|
||||
'Content-Type': 'text/xml',
|
||||
'Connection': 'close'
|
||||
});
|
||||
|
||||
const { error, value } = purchaseSchema.validate(req.body);
|
||||
if (error) throw new Error(`Invalid request: ${error.message}`);
|
||||
|
||||
const { original_id, original_password, move_point, item_code } = value;
|
||||
|
||||
if (!await validateCredentials(original_id, original_password)) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const currentBalance = await getCurrency(original_id);
|
||||
const newBalance = currentBalance + move_point;
|
||||
|
||||
if (newBalance < 0) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
|
||||
const item_id = extractItemId(item_code);
|
||||
await setCurrency(original_id, newBalance);
|
||||
|
||||
billingLogger.info(`[Billing] [JPN] Purchase processed for [${original_id}]
|
||||
Shop Item: ${item_id}
|
||||
Price: ${move_point}
|
||||
Balance: ${currentBalance} → ${newBalance}`);
|
||||
|
||||
return res.send(buildSuccessResponse(newBalance));
|
||||
|
||||
} catch (error) {
|
||||
billingLogger.error(`[Billing] [JPN] Purchase error: ${error.message}`);
|
||||
return res.send(buildErrorResponse(
|
||||
error.message.includes('Insufficient') ? 1 : 18,
|
||||
error.message.includes('Invalid') ? error.message : 'Transaction failed'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Routes
|
||||
router.post('/S1/ApiPointTotalGetS.php', urlencoded({ extended: true }), handleBalanceRequest);
|
||||
router.post('/S1/ApiPointMoveS.php', urlencoded({ extended: true }), handlePurchaseRequest);
|
||||
|
||||
export default router;
|
||||
146
src/routes/billingUsa.js
Normal file
146
src/routes/billingUsa.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { Router } from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import { parseStringPromise } from "xml2js";
|
||||
const router = Router();
|
||||
import { billingLogger } from "../utils/logger.js";
|
||||
import Joi from "joi";
|
||||
import {
|
||||
getCurrency,
|
||||
setCurrency,
|
||||
logBillingTransaction,
|
||||
} from "../services/accountDBService.js";
|
||||
|
||||
// Schemas
|
||||
const currencySchema = Joi.object({
|
||||
userid: Joi.string().required(),
|
||||
server: Joi.string().required(),
|
||||
game: Joi.string().required(),
|
||||
}).required();
|
||||
|
||||
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();
|
||||
|
||||
// XML response builders
|
||||
function buildSuccessResponse(balance) {
|
||||
return `<result><balance>${balance}</balance></result>`;
|
||||
}
|
||||
|
||||
function buildPurchaseSuccessResponse(newBalance) {
|
||||
return `<result>
|
||||
<status>success</status>
|
||||
<new-balance>${newBalance}</new-balance>
|
||||
</result>`;
|
||||
}
|
||||
|
||||
function buildErrorResponse(message) {
|
||||
return `<status>failed</status><message>${message}</message>`;
|
||||
}
|
||||
|
||||
// Currency request handler
|
||||
async function handleCurrencyRequest(data, res) {
|
||||
const { error, value } = currencySchema.validate(data);
|
||||
if (error) {
|
||||
billingLogger.warn(`[Billing] Invalid currency request: ${error.message}`);
|
||||
return res.status(400).send(buildErrorResponse("Invalid request"));
|
||||
}
|
||||
|
||||
const { userid, server } = value;
|
||||
|
||||
try {
|
||||
const currency = await getCurrency(userid);
|
||||
return res.send(buildSuccessResponse(currency));
|
||||
} catch (err) {
|
||||
billingLogger.error(
|
||||
`[Billing] Balance request error for [${userid}]: ${err.message}`
|
||||
);
|
||||
return res.status(400).send(buildErrorResponse("Failed to get balance"));
|
||||
}
|
||||
}
|
||||
|
||||
// Item purchase handler
|
||||
async function handleItemPurchaseRequest(data, res) {
|
||||
const { error, value } = itemPurchaseSchema.validate(data);
|
||||
if (error) {
|
||||
billingLogger.warn(`[Billing] Invalid purchase request: ${error.message}`);
|
||||
return res.status(400).send(buildErrorResponse("Invalid request"));
|
||||
}
|
||||
|
||||
const { userid, server, charid, uniqueid, amount, itemid, count } = value;
|
||||
|
||||
billingLogger.info(
|
||||
`[Billing] Processing purchase [${uniqueid}] for user [${userid}]`
|
||||
);
|
||||
|
||||
try {
|
||||
const balance = await getCurrency(userid);
|
||||
|
||||
if (amount <= 0) {
|
||||
return res.send(buildSuccessResponse(balance));
|
||||
}
|
||||
|
||||
if (amount > balance) {
|
||||
billingLogger.warn(
|
||||
`[Billing] Insufficient funds for user [${userid}]: Has ${balance}, needs ${amount}`
|
||||
);
|
||||
return res.status(400).send(buildErrorResponse("Insufficient funds"));
|
||||
}
|
||||
|
||||
const newBalance = balance - amount;
|
||||
await setCurrency(userid, newBalance);
|
||||
await logBillingTransaction({
|
||||
userId: userid,
|
||||
charId: charid,
|
||||
uniqueId: uniqueid,
|
||||
amount,
|
||||
itemId: itemid,
|
||||
count,
|
||||
});
|
||||
|
||||
billingLogger.info(`[Billing] Purchase successful for user [${userid}]
|
||||
Purchase ID: ${uniqueid}
|
||||
CharacterId: [${charid}]
|
||||
Item: ${itemid} (x${count})
|
||||
Price: ${amount}
|
||||
Balance: ${balance} → ${newBalance}`);
|
||||
|
||||
return res.send(buildPurchaseSuccessResponse(newBalance));
|
||||
} catch (err) {
|
||||
billingLogger.error(
|
||||
`[Billing] Purchase failed for user [${userid}]: ${err.message}`
|
||||
);
|
||||
return res.status(400).send(buildErrorResponse("Transaction failed"));
|
||||
}
|
||||
}
|
||||
|
||||
router.post("/", bodyParser.text({ type: "*/xml" }), async (req, res) => {
|
||||
res.set({
|
||||
"Content-Type": "text/xml",
|
||||
Connection: "close",
|
||||
});
|
||||
|
||||
try {
|
||||
const xml = req.body;
|
||||
const result = await parseStringPromise(xml, { explicitArray: false });
|
||||
|
||||
if (result["currency-request"]) {
|
||||
return handleCurrencyRequest(result["currency-request"], res);
|
||||
} else if (result["item-purchase-request"]) {
|
||||
return handleItemPurchaseRequest(result["item-purchase-request"], res);
|
||||
} else {
|
||||
return res.status(400).send(buildErrorResponse("Invalid request type"));
|
||||
}
|
||||
} catch (err) {
|
||||
billingLogger.error(`[Billing] Error handling billing request: ${err.message}`);
|
||||
return res.status(500).send(buildErrorResponse("Internal server error"));
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,58 +1,103 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const net = require('net');
|
||||
const { logger } = require('../utils/logger');
|
||||
import { Router } from 'express';
|
||||
const router = Router();
|
||||
import { Socket } from 'net';
|
||||
import config from '../config.js';
|
||||
const { ports, ips, logger } = config;
|
||||
import { create } from 'xmlbuilder2';
|
||||
|
||||
// Define the gateway route
|
||||
// Constants
|
||||
const SOCKET_TIMEOUT = 2000;
|
||||
const GATEWAY_STATUS = {
|
||||
ONLINE: 'online',
|
||||
OFFLINE: 'offline'
|
||||
};
|
||||
|
||||
/**
|
||||
* Build XML response for gateway info
|
||||
*/
|
||||
function buildGatewayXml() {
|
||||
return create({ version: '1.0', encoding: 'ISO-8859-1' })
|
||||
.ele('network')
|
||||
.ele('gateserver')
|
||||
.att('ip', ips.gate)
|
||||
.att('port', ports.gate)
|
||||
.up()
|
||||
.end({ prettyPrint: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Build gateway info route response
|
||||
*/
|
||||
function buildGatewayInfo(req) {
|
||||
const baseUrl = `${req.protocol}://${req.headers.host}`;
|
||||
return `1|${baseUrl}/launcher/GetGatewayAction|${baseUrl}/launcher/GetGatewayAction|`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check gateway server status via socket connection
|
||||
*/
|
||||
async function checkGatewayStatus() {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new Socket();
|
||||
socket.setTimeout(SOCKET_TIMEOUT);
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.destroy();
|
||||
resolve({ status: GATEWAY_STATUS.ONLINE });
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve({ status: GATEWAY_STATUS.OFFLINE, code: 408 });
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
socket.destroy();
|
||||
resolve({ status: GATEWAY_STATUS.OFFLINE, code: 503 });
|
||||
});
|
||||
|
||||
socket.connect(ports.gate, ips.gate);
|
||||
});
|
||||
}
|
||||
|
||||
// Main 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);
|
||||
try {
|
||||
res.set('Content-Type', 'application/xml');
|
||||
res.send(buildGatewayXml());
|
||||
logger.debug(`[Gateway] XML served to ${req.ip}`);
|
||||
} catch (error) {
|
||||
logger.error(`[Gateway] XML generation failed: ${error.message}`);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
// Define the gateway info route
|
||||
// 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);
|
||||
try {
|
||||
res.send(buildGatewayInfo(req));
|
||||
logger.debug(`[Gateway] Info served to ${req.ip}`);
|
||||
} catch (error) {
|
||||
logger.error(`[Gateway] Info generation failed: ${error.message}`);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
// Define the gateway status route
|
||||
// Gateway status route
|
||||
router.get('/status', async (req, res) => {
|
||||
const ip = process.env.GATESERVER_IP;
|
||||
const port = process.env.GATESERVER_PORT || '50001';
|
||||
try {
|
||||
const { status, code } = await checkGatewayStatus();
|
||||
|
||||
logger[status === GATEWAY_STATUS.ONLINE ? 'info' : 'warn'](
|
||||
`[Gateway] Status check from ${req.ip}: ${status}`
|
||||
);
|
||||
|
||||
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', () => {
|
||||
logger.info(`[Gateway] Connection attempt success from IP: ${req.ip}`);
|
||||
res.status(200).json({ status: 'online' });
|
||||
socket.destroy();
|
||||
});
|
||||
socket.on('timeout', () => {
|
||||
logger.warn(`[Gateway] Connection attempt timeout from IP: ${req.ip}`);
|
||||
res.status(408).json({ status: 'offline' });
|
||||
socket.destroy();
|
||||
});
|
||||
socket.on('error', () => {
|
||||
logger.error(`[Gateway] Connection failed from IP: ${req.ip}`);
|
||||
res.status(503).json({ status: 'offline' });
|
||||
socket.destroy();
|
||||
});
|
||||
res.status(status === GATEWAY_STATUS.ONLINE ? 200 : code || 503)
|
||||
.json({ status });
|
||||
} catch (error) {
|
||||
logger.error(`[Gateway] Status check failed: ${error.message}`);
|
||||
res.status(500).json({ status: GATEWAY_STATUS.OFFLINE });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
@ -1,108 +1,60 @@
|
|||
const sql = require('mssql');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
const { logger, accountLogger } = require('../../utils/logger');
|
||||
const { sendPasswordChangedEmail } = require('../../mailer/mailer');
|
||||
const Joi = require('joi');
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
import joi from "joi";
|
||||
import { logger, accountLogger } from "../../utils/logger.js";
|
||||
import { changeAccountPassword } from "../../services/accountDBService.js";
|
||||
|
||||
// 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()
|
||||
const schema = joi.object({
|
||||
email: joi.string().email().required(),
|
||||
password: joi.string().required(),
|
||||
verificationCode: joi.string().pattern(new RegExp("^[0-9]+$")).required(),
|
||||
});
|
||||
|
||||
// Route for registering an account
|
||||
router.post('/', async (req, res) => {
|
||||
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);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
result: "ValidationError",
|
||||
message: error.details[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
const email = value.email;
|
||||
const password = value.password;
|
||||
const verificationCode = value.verification_code;
|
||||
const { email, password, verificationCode } = value;
|
||||
|
||||
// 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];
|
||||
const changeAccountPasswordStatus = await changeAccountPassword(
|
||||
email,
|
||||
password,
|
||||
verificationCode
|
||||
);
|
||||
|
||||
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
|
||||
switch (changeAccountPasswordStatus) {
|
||||
case "PasswordChanged":
|
||||
accountLogger.info(
|
||||
`[Account] Account [${email}] password changed successfully`
|
||||
);
|
||||
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);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
result: changeAccountPasswordStatus,
|
||||
});
|
||||
default:
|
||||
accountLogger.info(
|
||||
`[Account] Account [${email}] password change failed: ${changeAccountPasswordStatus}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
result: changeAccountPasswordStatus,
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[Account] A error ocourred: ' + error.message);
|
||||
return res.status(500).send('A error ocourred. Please try again later.');
|
||||
logger.error(`Account password change failed: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
result: "ServerError",
|
||||
message: "A server error occurred. Please try again later.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,53 +1,63 @@
|
|||
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');
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
import joi from "joi";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { verifyCode } from "../../services/accountDBService.js";
|
||||
|
||||
// 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()
|
||||
const schema = joi.object({
|
||||
email: joi.string().email().required(),
|
||||
verificationCodeType: joi.string().required(),
|
||||
verificationCode: joi.string().pattern(new RegExp("^[0-9]+$")).required(),
|
||||
});
|
||||
|
||||
// Route for registering an account
|
||||
router.post('/', async (req, res) => {
|
||||
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);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
result: "ValidationError",
|
||||
message: 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');
|
||||
}
|
||||
const {
|
||||
email,
|
||||
verificationCode: verificationCode,
|
||||
verificationCodeType: verificationCodeType,
|
||||
} = value;
|
||||
|
||||
// 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('A error ocourred. Please try again later.');
|
||||
// Verify code with database
|
||||
const verificationResult = await verifyCode(
|
||||
email,
|
||||
verificationCode,
|
||||
verificationCodeType
|
||||
);
|
||||
if (verificationResult !== "ValidVerificationCode") {
|
||||
logger.info(
|
||||
`[Account] Verification failed for ${email}. Status: ${verificationResult}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
result: verificationResult
|
||||
});
|
||||
}
|
||||
logger.info(
|
||||
`[Account] Verification successful for ${email}. Status: ${verificationResult}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
result: verificationResult,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Verification failed: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
result: "ServerError",
|
||||
message: "A server error occurred. Please try again later.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,41 +1,120 @@
|
|||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { logger } = require('../../utils/logger');
|
||||
import { Router } from "express";
|
||||
import { readFile, existsSync } from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const router = express.Router();
|
||||
const router = 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');
|
||||
});
|
||||
router.get("/getLauncherVersion", (req, res) => {
|
||||
try {
|
||||
const launcherInfoPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"public",
|
||||
"launcher",
|
||||
"launcher_update",
|
||||
"launcher_info.ini"
|
||||
);
|
||||
|
||||
readFile(launcherInfoPath, "utf8", (err, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
return res.status(400).json({
|
||||
result: "FileReadError",
|
||||
message: "Error reading launcher_info.ini file",
|
||||
});
|
||||
}
|
||||
|
||||
const versionRegex = /version=(.*)/i;
|
||||
const match = data.match(versionRegex);
|
||||
if (match) {
|
||||
const launcherVersion = match[1];
|
||||
return res.status(200).json({
|
||||
version: launcherVersion,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).json({
|
||||
result: "VersionNotFound",
|
||||
message: "Version not found in launcher_info.ini file",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[getLauncherVersion] Processing error: ${error}`);
|
||||
return res.status(500).json({
|
||||
result: "InternalError",
|
||||
message: "An error occurred while processing the file",
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[getLauncherVersion] Initialization error: ${error}`);
|
||||
return res.status(500).json({
|
||||
result: "InternalError",
|
||||
message: "An error occurred while initializing the version check",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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`);
|
||||
router.post("/updateLauncherVersion", (req, res) => {
|
||||
try {
|
||||
const launcherUpdatePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"public",
|
||||
"launcher",
|
||||
"launcher_update"
|
||||
);
|
||||
|
||||
const version = req.body.version;
|
||||
if (!version) {
|
||||
return res.status(400).json({
|
||||
result: "MissingVersion",
|
||||
message: "Missing version parameter",
|
||||
});
|
||||
}
|
||||
|
||||
const fileName = `launcher_${version}.zip`;
|
||||
const file = path.join(launcherUpdatePath, fileName);
|
||||
|
||||
if (!existsSync(file)) {
|
||||
logger.error(`[Launcher Updater] File ${fileName} not found`);
|
||||
return res.status(404).json({
|
||||
result: "FileNotFound",
|
||||
message: `File ${fileName} not found on the server`,
|
||||
});
|
||||
}
|
||||
|
||||
res.download(file, (err) => {
|
||||
if (err) {
|
||||
logger.error(`[updateLauncherVersion] Download error: ${err}`);
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).json({
|
||||
result: "DownloadError",
|
||||
message: "Error occurred while downloading the file",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[updateLauncherVersion] Error: ${error}`);
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).json({
|
||||
result: "InternalError",
|
||||
message: "An unexpected error occurred",
|
||||
});
|
||||
}
|
||||
}
|
||||
res.download(file);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
@ -1,99 +1,63 @@
|
|||
const sql = require('mssql');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { logger, accountLogger } = require('../../utils/logger');
|
||||
const Joi = require('joi');
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
import joi from "joi";
|
||||
import config from "../../config.js";
|
||||
const { apiConfig } = config;
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { getClientIp } from "../../utils/getClientIp.js";
|
||||
import { authenticateUser } from "../../services/accountDBService.js";
|
||||
|
||||
// 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(),
|
||||
const schema = joi.object({
|
||||
account: joi.string().required(),
|
||||
password: joi.string().min(8).max(16).required(),
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
// Validate the request body against the schema
|
||||
// Validate request
|
||||
const { error, value } = schema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).send(error.details[0].message);
|
||||
return res.status(400).json({
|
||||
result: "InvalidRequest",
|
||||
message: error.details[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
const account = req.body.account;
|
||||
const password = req.body.password;
|
||||
const userIp = req.ip;
|
||||
const { account, password } = value;
|
||||
const ip = getClientIp(req);
|
||||
|
||||
// Check the format of the account identifier
|
||||
if (
|
||||
!/^[a-z0-9_-]{6,50}$/.test(account) &&
|
||||
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account)
|
||||
) {
|
||||
return res.status(400).json({ Result: 'InvalidUsernameFormat' });
|
||||
}
|
||||
|
||||
// Use a prepared statement to retrieve the account information
|
||||
const pool = await connAccount;
|
||||
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
|
||||
logger.info(
|
||||
apiConfig.logIPAddresses === "true"
|
||||
? `[Launcher Login] Account [${account}] is trying to login from [${ip}]`
|
||||
: `[Launcher Login] Account [${account}] is trying to login`
|
||||
);
|
||||
return res.status(500).send('Login failed. Please try again later.');
|
||||
// Authenticate user
|
||||
const authResult = await authenticateUser(account, password, ip);
|
||||
|
||||
if (!authResult || authResult.status !== "LoginSuccess") {
|
||||
logger.warn(
|
||||
`[Launcher Login] Authentication failed for user [${account}]: ${authResult?.status}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
result: authResult?.status || "AuthenticationFailed",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[Launcher Login] Authentication successful for user [${account}]`
|
||||
);
|
||||
return res.status(200).json({
|
||||
result: authResult.status,
|
||||
token: authResult.token,
|
||||
windyCode: account,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[Launcher Login] Authentication failed: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
result: "ServerError",
|
||||
message: "A server error occurred. Please try again later.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,74 +1,52 @@
|
|||
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');
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
import { logger, accountLogger } from "../../utils/logger.js";
|
||||
import joi from "joi";
|
||||
import { sendPasswordVerificationEmail } from "../../services/accountDBService.js";
|
||||
|
||||
// Set up database connection
|
||||
const { connAccount } = require('../../utils/dbConfig');
|
||||
|
||||
// Joi schema for request body validation
|
||||
const schema = Joi.object({
|
||||
email: Joi.string().email().required()
|
||||
const schema = joi.object({
|
||||
email: joi.string().email().required(),
|
||||
});
|
||||
|
||||
// Route for registering an account
|
||||
router.post('/', async (req, res) => {
|
||||
// Route for sending verification email for password reset
|
||||
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);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
result: "ValidationError",
|
||||
message: error.details[0].message,
|
||||
});
|
||||
}
|
||||
const email = req.body.email;
|
||||
const { email } = value;
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
logger.info('Invalid email format');
|
||||
return res.status(400).send('InvalidEmailFormat');
|
||||
}
|
||||
const sendEmailStatus = await sendPasswordVerificationEmail(email);
|
||||
|
||||
// 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.error(`[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('A error ocourred. Please try again later.');
|
||||
if (sendEmailStatus !== "Success") {
|
||||
accountLogger.info(
|
||||
`[Account] Password reset request failed to [${email}]. Status: ${sendEmailStatus}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
result: sendEmailStatus,
|
||||
});
|
||||
}
|
||||
accountLogger.info(
|
||||
`[Account] Password reset request sent to [${email}]. Status: ${sendEmailStatus}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
result: sendEmailStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[Account] Password reset request failed: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
result: 'ServerError',
|
||||
message: 'A server error occurred. Please try again later.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
const sql = require('mssql');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
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(6).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;
|
||||
|
||||
if (
|
||||
!/^[a-z0-9_-]{6,50}$/.test(windyCode) &&
|
||||
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(email)
|
||||
) {
|
||||
return res.status(400).send('InvalidUsernameFormat');
|
||||
}
|
||||
|
||||
const md5_password = crypto.createHash('md5').update(windyCode + password).digest('hex'); // Generate MD5 hash
|
||||
|
||||
const passwordHash = await bcrypt.hash(md5_password, 10);
|
||||
|
||||
// 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.error(`[Account] Account [${windyCode}] creation failed: ${row.Result}`);
|
||||
return res.status(400).send(row.Result);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[Account] Database query failed: ' + error.message);
|
||||
return res.status(500).send('A error ocourred. Please try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
65
src/routes/launcher/registerAccount.js
Normal file
65
src/routes/launcher/registerAccount.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { Router } from "express";
|
||||
const router = Router();
|
||||
import joi from "joi";
|
||||
import { logger, accountLogger } from "../../utils/logger.js";
|
||||
import { createAccount } from "../../services/accountDBService.js";
|
||||
import { getClientIp } from "../../utils/getClientIp.js";
|
||||
|
||||
const schema = joi.object({
|
||||
username: joi.string().alphanum().lowercase().min(6).max(16).required(),
|
||||
email: joi.string().email().required(),
|
||||
password: joi.string().min(8).max(16).required(),
|
||||
verificationCode: joi.string().pattern(new RegExp("^[0-9]+$")).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).json({
|
||||
success: false,
|
||||
result: "ValidationError",
|
||||
message: error.details[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
const ip = getClientIp(req);
|
||||
const { username, email, password, verificationCode } = value;
|
||||
|
||||
const createAccountStatus = await createAccount(
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
ip,
|
||||
verificationCode
|
||||
);
|
||||
|
||||
switch (createAccountStatus) {
|
||||
case "AccountCreated":
|
||||
accountLogger.info(
|
||||
`[Account] Account [${username}] created successfully.`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
result: createAccountStatus,
|
||||
});
|
||||
default:
|
||||
accountLogger.info(
|
||||
`[Account] Account [${username}] creation failed - ${createAccountStatus}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
result: createAccountStatus,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[Account] Account creation failed: " + error.message);
|
||||
return res.status(500).json({
|
||||
result: "ServerError",
|
||||
message: "A server error occurred. Please try again later.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,68 +1,54 @@
|
|||
// Load environment variables
|
||||
const env = require('../../utils/env');
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
import { logger, accountLogger } from "../../utils/logger.js";
|
||||
import joi from "joi";
|
||||
import { sendAccountVerificationEmail } from "../../services/accountDBService.js";
|
||||
|
||||
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()
|
||||
const schema = joi.object({
|
||||
email: joi.string().email().required(),
|
||||
});
|
||||
|
||||
// Route for registering an account
|
||||
router.post('/', async (req, res) => {
|
||||
// Route for sending verification email for account creation
|
||||
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);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
result: "ValidationError",
|
||||
message: error.details[0].message,
|
||||
});
|
||||
}
|
||||
const email = req.body.email;
|
||||
const { email } = value;
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return res.status(400).send('InvalidEmailFormat');
|
||||
}
|
||||
const sendEmailStatus = await sendAccountVerificationEmail(email);
|
||||
|
||||
// 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('A error ocourred. Please try again later.');
|
||||
if (sendEmailStatus !== "Success") {
|
||||
accountLogger.info(
|
||||
`[Account] Verification email sending failed to ${email}. Sending status: ${sendEmailStatus}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
result: sendEmailStatus,
|
||||
});
|
||||
}
|
||||
accountLogger.info(
|
||||
`[Account] Verification email sent to ${email}. Sending status: ${sendEmailStatus}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
result: sendEmailStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[Account] Verification email sending failed: ${error.message}`
|
||||
);
|
||||
return res.status(500).json({
|
||||
result: 'ServerError',
|
||||
message: 'A server error occurred. Please try again later.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,47 +1,96 @@
|
|||
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');
|
||||
import { Router } from 'express';
|
||||
import sql from 'mssql';
|
||||
const router = Router();
|
||||
import NodeCache from 'node-cache';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { authDBConfig } from '../utils/dbConfig.js';
|
||||
import { fetchOnlineCount } from "../services/authDBService.js";
|
||||
|
||||
// Set up the cache
|
||||
const cache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
|
||||
const CACHE_KEY = 'onlineCount';
|
||||
const CACHE_TTL = 60; // 1 minute
|
||||
const CHECK_PERIOD = 120; // 2 minutes
|
||||
|
||||
// Route for getting the count of online players
|
||||
// Cache configuration
|
||||
const cache = new NodeCache({
|
||||
stdTTL: CACHE_TTL,
|
||||
checkperiod: CHECK_PERIOD,
|
||||
useClones: false
|
||||
});
|
||||
|
||||
// Database connection pool
|
||||
let pool;
|
||||
let poolReady = (async () => {
|
||||
try {
|
||||
pool = await new sql.ConnectionPool(authDBConfig).connect();
|
||||
} catch (error) {
|
||||
logger.error('Failed to create database connection pool:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Gets online count, using cache when possible
|
||||
*/
|
||||
async function getOnlineCount() {
|
||||
try {
|
||||
// Try to get from cache first
|
||||
let count = cache.get(CACHE_KEY);
|
||||
|
||||
if (count === undefined) {
|
||||
logger.debug('Cache miss for online count, querying database');
|
||||
count = await fetchOnlineCount(pool);
|
||||
cache.set(CACHE_KEY, count);
|
||||
} else {
|
||||
logger.debug('Online count retrieved from cache');
|
||||
}
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
// If we have a cached value, return it even if the query fails
|
||||
const cached = cache.get(CACHE_KEY);
|
||||
if (cached !== undefined) {
|
||||
logger.warn('Using cached online count due to database error');
|
||||
return cached;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Route for getting online players count
|
||||
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 });
|
||||
if (!pool) await poolReady;
|
||||
|
||||
const count = await getOnlineCount();
|
||||
|
||||
// Set cache-control headers
|
||||
res.set('Cache-Control', `public, max-age=${CACHE_TTL}`);
|
||||
|
||||
return res.status(200).json({
|
||||
count,
|
||||
cached: cache.has(CACHE_KEY),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Database query failed: ' + error.message);
|
||||
return res.status(500).send('Database query failed. Please try again later.');
|
||||
logger.error('Failed to get online count:', error);
|
||||
return res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: 'Unable to retrieve online player count'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
// Cleanup on process exit
|
||||
process.on('SIGINT', async () => {
|
||||
try {
|
||||
if (pool) {
|
||||
await pool.close();
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('Error closing connection pool:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
18
src/servers/jpnApp.js
Normal file
18
src/servers/jpnApp.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import express, { urlencoded } from 'express';
|
||||
import config from '../config.js';
|
||||
const { ports, ips, logger } = config;
|
||||
import authJpnRouter from '../routes/authJpn.js';
|
||||
import billingJpnRouter from '../routes/billingJpn.js';
|
||||
|
||||
const app = express();
|
||||
app.use(urlencoded({ extended: false, type: 'application/x-www-form-urlencoded' }));
|
||||
app.use('/Auth', authJpnRouter);
|
||||
app.use('/Billing', billingJpnRouter);
|
||||
|
||||
const startServer = () => {
|
||||
return app.listen(ports.jpnApp, ips.local, () => {
|
||||
logger.info(`API (JPN) listening on ${ips.local}:${ports.jpnApp}`);
|
||||
});
|
||||
};
|
||||
|
||||
export { app, startServer };
|
||||
85
src/servers/mainApp.js
Normal file
85
src/servers/mainApp.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import express from 'express';
|
||||
import config from '../config.js';
|
||||
const { ports, ips, apiConfig, middleware, staticPaths, logger } = config;
|
||||
import rateLimiter from '../lib/rateLimiter.js';
|
||||
import { closeConnection } from '../lib/closeConnection.js';
|
||||
import path from 'path';
|
||||
|
||||
// Routers
|
||||
import gatewayRouter from '../routes/gateway.js';
|
||||
import loginRouter from '../routes/launcher/login.js';
|
||||
import registerRouter from '../routes/launcher/registerAccount.js';
|
||||
import codeVerificationRouter from '../routes/launcher/codeVerification.js';
|
||||
import passwordResetEmailRouter from '../routes/launcher/passwordResetEmail.js';
|
||||
import passwordChangeRouter from '../routes/launcher/changePassword.js';
|
||||
import verificationEmailRouter from '../routes/launcher/verificationEmail.js';
|
||||
import launcherUpdaterRouter from '../routes/launcher/launcherUpdater.js';
|
||||
import onlineCountRouter from '../routes/onlineCount.js';
|
||||
|
||||
const app = express();
|
||||
if (apiConfig.trustProxyEnabled) {
|
||||
const trustProxyHosts = apiConfig.trustProxyHosts || [];
|
||||
|
||||
if (trustProxyHosts.length > 0) {
|
||||
app.set("trust proxy", trustProxyHosts);
|
||||
} else {
|
||||
app.set("trust proxy", true);
|
||||
}
|
||||
}
|
||||
app.disable("x-powered-by");
|
||||
app.disable("etag");
|
||||
|
||||
// Middleware
|
||||
app.use(...middleware.getMiddleware());
|
||||
|
||||
// Routes
|
||||
app.use('/launcher/GetGatewayAction', closeConnection, rateLimiter, gatewayRouter);
|
||||
app.use('/launcher/SignupAction', closeConnection, rateLimiter, registerRouter);
|
||||
app.use('/launcher/LoginAction', closeConnection, rateLimiter, loginRouter);
|
||||
app.use('/launcher/VerifyCodeAction', closeConnection, rateLimiter, codeVerificationRouter);
|
||||
app.use('/launcher/ResetPasswordAction', closeConnection, rateLimiter, passwordChangeRouter);
|
||||
app.use('/launcher/SendPasswordResetEmailAction', closeConnection, rateLimiter, passwordResetEmailRouter);
|
||||
app.use('/launcher/SendVerificationEmailAction', closeConnection, rateLimiter, verificationEmailRouter);
|
||||
app.use('/launcherAction', closeConnection, rateLimiter, launcherUpdaterRouter);
|
||||
app.use('/launcher/GetOnlineCountAction', closeConnection, rateLimiter, onlineCountRouter);
|
||||
|
||||
// Static files
|
||||
app.use(express.static(staticPaths.public));
|
||||
app.use('/launcher/news', express.static(staticPaths.launcherNews));
|
||||
app.use('/site', express.static(staticPaths.site));
|
||||
app.use('/launcher/patch', express.static(staticPaths.launcherPatch));
|
||||
app.use('/launcher/client', express.static(staticPaths.launcherClient));
|
||||
|
||||
// HTML routes
|
||||
app.get('/launcher/news', (req, res) => {
|
||||
res.sendFile(path.join(staticPaths.launcherNews, 'news-panel.html'));
|
||||
});
|
||||
|
||||
app.get('/launcher/agreement', (req, res) => {
|
||||
res.sendFile(path.join(staticPaths.launcherNews, 'agreement.html'));
|
||||
});
|
||||
|
||||
app.get('/favicon.ico', (req, res) => {
|
||||
res.sendFile(path.join(staticPaths.launcherNews, 'favicon.ico'));
|
||||
});
|
||||
|
||||
app.get('/Register', (req, res) => {
|
||||
res.sendFile(path.join(staticPaths.site, 'Signup.html'));
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error(err.stack);
|
||||
return res.status(500).json({
|
||||
result: 'ServerError',
|
||||
message: 'A server error occurred. Please try again later.'
|
||||
});
|
||||
});
|
||||
|
||||
const startServer = () => {
|
||||
return app.listen(ports.main, ips.public, () => {
|
||||
logger.info(`API listening on ${ips.public}:${ports.main}`);
|
||||
});
|
||||
};
|
||||
|
||||
export { app, startServer };
|
||||
151
src/servers/proxyServer.js
Normal file
151
src/servers/proxyServer.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { createServer } from 'net';
|
||||
import { request } from 'http';
|
||||
import config from '../config.js';
|
||||
|
||||
const { ports, ips, logger, BACKENDS } = config;
|
||||
|
||||
const parseRequest = (data) => {
|
||||
const lines = data.split('\r\n');
|
||||
let realPath = '/';
|
||||
let isMalformed = false;
|
||||
|
||||
if (lines.some(line => line.startsWith('POST /') && line.includes('HTTP/1.1Content-Type:'))) {
|
||||
isMalformed = true;
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('POST /') && line.includes('HTTP/1.1Content-Type:')) {
|
||||
const match = line.match(/POST (\/[^ ]*) HTTP\/1\.1/);
|
||||
if (match) realPath = match[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requestLine = lines[0];
|
||||
const match = requestLine.match(/^(POST|GET) (\/(?:[^ ]*)) HTTP\/1\.1$/i);
|
||||
if (match) realPath = match[2];
|
||||
}
|
||||
|
||||
if (realPath.startsWith('/cgi-bin/')) {
|
||||
realPath = '/Auth' + realPath;
|
||||
} else if (realPath.startsWith('/S1/')) {
|
||||
realPath = '/Billing' + realPath;
|
||||
}
|
||||
|
||||
return { realPath, isMalformed, lines };
|
||||
};
|
||||
|
||||
const findBackend = (realPath) => {
|
||||
if (BACKENDS.AUTH.paths.includes(realPath)) {
|
||||
return { ...BACKENDS.AUTH, port: ports.jpnApp };
|
||||
} else if (BACKENDS.BILLING.paths.some(path => realPath.startsWith(path))) {
|
||||
return { ...BACKENDS.BILLING, port: ports.jpnApp };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildHeaders = (lines, body, backend, isMalformed) => {
|
||||
const headers = {
|
||||
'Host': `${ips.local}:${backend.port}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'User-Agent': 'Mozilla/4.0 (ISAO/1.00;Auth)',
|
||||
};
|
||||
|
||||
if (!isMalformed) {
|
||||
const headerLines = lines.slice(1, lines.indexOf(''));
|
||||
for (const line of headerLines) {
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = line.substring(0, colonIndex).trim().toLowerCase();
|
||||
const value = line.substring(colonIndex + 1).trim();
|
||||
if (['content-type', 'user-agent', 'content-length'].includes(key)) {
|
||||
headers[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
const handleProxyRequest = (socket, data) => {
|
||||
try {
|
||||
const { realPath, isMalformed, lines } = parseRequest(data);
|
||||
|
||||
const backend = findBackend(realPath);
|
||||
if (!backend) {
|
||||
logger.error('[Proxy] Unknown path:', realPath);
|
||||
socket.end('HTTP/1.1 404 Not Found\r\n\r\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = data.split('\r\n\r\n')[1] || '';
|
||||
const headers = buildHeaders(lines, body, backend, isMalformed);
|
||||
|
||||
const options = {
|
||||
hostname: ips.local,
|
||||
port: backend.port,
|
||||
path: realPath,
|
||||
method: 'POST',
|
||||
headers,
|
||||
};
|
||||
|
||||
const proxyReq = request(options, (proxyRes) => {
|
||||
socket.write(`HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}\r\n`);
|
||||
Object.entries(proxyRes.headers).forEach(([key, value]) => {
|
||||
socket.write(`${key}: ${value}\r\n`);
|
||||
});
|
||||
socket.write('\r\n');
|
||||
proxyRes.pipe(socket);
|
||||
});
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
logger.error('[Proxy] Proxy error:', err);
|
||||
socket.end('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
||||
});
|
||||
|
||||
proxyReq.write(body);
|
||||
proxyReq.end();
|
||||
|
||||
} catch (err) {
|
||||
logger.error('[Proxy] Error parsing request:', err);
|
||||
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
}
|
||||
};
|
||||
|
||||
const createProxyServer = () => {
|
||||
const server = createServer((socket) => {
|
||||
let data = '';
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
data += chunk.toString('binary');
|
||||
|
||||
if (data.includes('\r\n\r\n')) {
|
||||
handleProxyRequest(socket, data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
logger.error('[Proxy] Socket error:', err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
logger.warn('[Proxy] Socket timeout.');
|
||||
socket.end();
|
||||
});
|
||||
});
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
const startServer = () => {
|
||||
const server = createProxyServer();
|
||||
return server.listen(ports.proxy, ips.local, () => {
|
||||
logger.info(`Proxy (JPN) listening on ${ips.local}:${ports.proxy}`);
|
||||
console.log('Configured backends:');
|
||||
console.log(`- AUTH (${ports.jpnApp}):`, BACKENDS.AUTH.paths);
|
||||
console.log(`- BILLING (${ports.jpnApp}):`, BACKENDS.BILLING.paths);
|
||||
});
|
||||
};
|
||||
|
||||
export { createProxyServer, startServer };
|
||||
17
src/servers/usaApp.js
Normal file
17
src/servers/usaApp.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import express from 'express';
|
||||
import config from '../config.js';
|
||||
const { ports, ips, logger } = config;
|
||||
import authUsaRouter from '../routes/authUsa.js';
|
||||
import billingRouter from '../routes/billingUsa.js';
|
||||
|
||||
const app = express();
|
||||
app.use('/Auth', authUsaRouter);
|
||||
app.use('/Billing', billingRouter);
|
||||
|
||||
const startServer = () => {
|
||||
return app.listen(ports.usaApp, ips.local, () => {
|
||||
logger.info(`API (USA) listening on ${ips.local}:${ports.usaApp}`);
|
||||
});
|
||||
};
|
||||
|
||||
export { app, startServer };
|
||||
334
src/services/accountDBService.js
Normal file
334
src/services/accountDBService.js
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
import sql from "mssql";
|
||||
import {
|
||||
generateMD5Hash,
|
||||
comparePassword,
|
||||
hashPassword,
|
||||
} from "../utils/hashUtils.js";
|
||||
import { connAccount } from "../utils/dbConfig.js";
|
||||
import configs from "../config.js";
|
||||
const { config } = configs;
|
||||
import {
|
||||
sendConfirmationEmail,
|
||||
sendVerificationEmail,
|
||||
sendPasswordResetEmail,
|
||||
sendPasswordChangedEmail,
|
||||
} from "../mailer/mailer.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
// ==============================================
|
||||
// Account Management Functions
|
||||
// ==============================================
|
||||
|
||||
export async function getAccount(identifier) {
|
||||
const pool = connAccount;
|
||||
const result = await pool
|
||||
.request()
|
||||
.input("Identifier", sql.VarChar(50), identifier)
|
||||
.execute("GetAccount");
|
||||
|
||||
const row = result.recordset[0];
|
||||
if (!row || row.Result !== "AccountExists") {
|
||||
throw new Error(row?.Result || "AccountNotFound");
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function createAccount(account, email, password, ip, verificationCode) {
|
||||
const isValidVerificationCode = await verifyCode(
|
||||
email,
|
||||
verificationCode,
|
||||
"Account"
|
||||
);
|
||||
if (isValidVerificationCode !== "ValidVerificationCode") {
|
||||
return isValidVerificationCode;
|
||||
}
|
||||
|
||||
const md5Password = generateMD5Hash(account, password);
|
||||
const passwordHash = await hashPassword(md5Password, 10);
|
||||
|
||||
const pool = connAccount;
|
||||
const request = pool.request();
|
||||
request.input("WindyCode", sql.VarChar, account);
|
||||
request.input("AccountPwd", sql.VarChar, passwordHash);
|
||||
request.input("Email", sql.VarChar, email);
|
||||
request.input("RegisterIP", sql.VarChar, ip);
|
||||
request.input("ServerId", sql.Int, config.serverId);
|
||||
request.input("ShopBalance", sql.BigInt, config.shopBalance);
|
||||
const result = await request.execute("CreateAccount");
|
||||
const row = result.recordset[0];
|
||||
|
||||
if (row.Result == "AccountCreated") {
|
||||
sendConfirmationEmail(email, account);
|
||||
await clearVerificationCode(email);
|
||||
}
|
||||
|
||||
return row.Result;
|
||||
}
|
||||
|
||||
export async function validateCredentials(account, password) {
|
||||
try {
|
||||
const accountStatus = await getAccount(account);
|
||||
const passwordHash = generateMD5Hash(account, password);
|
||||
const passwordMatch = await comparePassword(
|
||||
passwordHash,
|
||||
accountStatus.AccountPwd
|
||||
);
|
||||
return accountStatus.Result == "AccountExists" && passwordMatch;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Authentication Functions
|
||||
// ==============================================
|
||||
|
||||
export async function authenticateUser(account, password, ip, isMd5 = false) {
|
||||
const pool = connAccount;
|
||||
|
||||
// Get account info
|
||||
const { recordset } = await pool
|
||||
.request()
|
||||
.input("Identifier", sql.VarChar(50), account)
|
||||
.execute("GetAccount");
|
||||
|
||||
const row = recordset[0];
|
||||
if (!row || row.Result !== "AccountExists") {
|
||||
return { status: row.Result };
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const passwordHash = isMd5 ? password : generateMD5Hash(account, password);
|
||||
const passwordMatch = await comparePassword(passwordHash, row.AccountPwd);
|
||||
|
||||
// Authenticate
|
||||
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") {
|
||||
return { status: authRow.Result };
|
||||
}
|
||||
|
||||
return {
|
||||
status: authRow.Result,
|
||||
authId: authRow.AuthID,
|
||||
token: authRow.Token,
|
||||
};
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Password Management Functions
|
||||
// ==============================================
|
||||
|
||||
export async function changeAccountPassword(email, password, verificationCode) {
|
||||
|
||||
// Get account information
|
||||
const accountInfo = await getAccount(email);
|
||||
if (!accountInfo || accountInfo.Result !== "AccountExists") {
|
||||
return accountInfo;
|
||||
}
|
||||
|
||||
// Check verification code
|
||||
const verificationResult = await verifyCode(
|
||||
email,
|
||||
verificationCode,
|
||||
"Password"
|
||||
);
|
||||
|
||||
if (!verificationResult || verificationResult !== "ValidVerificationCode") {
|
||||
return verificationResult;
|
||||
}
|
||||
|
||||
const accountName = accountInfo.WindyCode;
|
||||
const currentHash = accountInfo.AccountPwd;
|
||||
const passwordHash = generateMD5Hash(accountName, password);
|
||||
|
||||
// Verify if password is the same
|
||||
const isSamePassword = await comparePassword(passwordHash, currentHash);
|
||||
if (isSamePassword) {
|
||||
return "SamePassword";
|
||||
}
|
||||
|
||||
// Hash and update password
|
||||
const newPasswordHash = await hashPassword(passwordHash);
|
||||
const updateResult = await updatePassword(email, newPasswordHash);
|
||||
|
||||
if (!updateResult || updateResult.Result !== "PasswordChanged") {
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
// Clear verification code and send email
|
||||
await clearVerificationCode(email);
|
||||
sendPasswordChangedEmail(email, accountName);
|
||||
|
||||
return updateResult.Result;
|
||||
}
|
||||
|
||||
export async function updatePassword(email, newPasswordHash) {
|
||||
const pool = connAccount;
|
||||
const result = await pool
|
||||
.request()
|
||||
.input("Email", sql.VarChar, email)
|
||||
.input("AccountPwd", sql.VarChar, newPasswordHash)
|
||||
.execute("UpdateAccountPassword");
|
||||
|
||||
return result.recordset[0];
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Verification Code Functions
|
||||
// ==============================================
|
||||
|
||||
export function generateVerificationCode() {
|
||||
const verificationCode = Math.floor(10000 + Math.random() * 90000).toString();
|
||||
|
||||
// Set the expiration time 10 minutes from now in the specified timezone
|
||||
const expirationTime = new Date(Date.now() + 600000).toLocaleString(
|
||||
"en-US",
|
||||
config.timeZone
|
||||
);
|
||||
|
||||
return {
|
||||
code: verificationCode,
|
||||
expiration: expirationTime,
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendAccountVerificationEmail(email) {
|
||||
const verificationCode = generateVerificationCode();
|
||||
|
||||
const insertRow = await setAccountVerificationCode(
|
||||
email,
|
||||
verificationCode.code,
|
||||
verificationCode.expiration
|
||||
);
|
||||
|
||||
if (insertRow.Result == "Success") {
|
||||
// Send verification code email
|
||||
sendVerificationEmail(email, verificationCode.code);
|
||||
return "EmailSent";
|
||||
} else {
|
||||
return insertRow.Result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendPasswordVerificationEmail(email) {
|
||||
const verificationCode = generateVerificationCode();
|
||||
|
||||
const insertRow = await setPasswordVerificationCode(
|
||||
email,
|
||||
verificationCode.code,
|
||||
verificationCode.expiration
|
||||
);
|
||||
|
||||
if (insertRow.Result == "Success") {
|
||||
// Send verification code email
|
||||
sendPasswordResetEmail(email, verificationCode.code);
|
||||
return "EmailSent";
|
||||
} else {
|
||||
return insertRow.Result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setAccountVerificationCode(
|
||||
email,
|
||||
verificationCode,
|
||||
expirationTime
|
||||
) {
|
||||
const pool = connAccount;
|
||||
const result = await pool
|
||||
.request()
|
||||
.input("Email", sql.VarChar, email)
|
||||
.input("VerificationCode", sql.VarChar, verificationCode)
|
||||
.input("ExpirationTime", sql.DateTime, expirationTime)
|
||||
.execute("SetAccountVerificationCode");
|
||||
const row = result.recordset[0];
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function setPasswordVerificationCode(
|
||||
email,
|
||||
verificationCode,
|
||||
expirationTime
|
||||
) {
|
||||
const pool = connAccount;
|
||||
const result = await pool
|
||||
.request()
|
||||
.input("Email", sql.VarChar, email)
|
||||
.input("VerificationCode", sql.VarChar, verificationCode)
|
||||
.input("ExpirationTime", sql.DateTime, expirationTime)
|
||||
.execute("SetPasswordVerificationCode");
|
||||
return result.recordset[0];
|
||||
}
|
||||
|
||||
export async function verifyCode(email, verificationCode, verificationCodeType) {
|
||||
const pool = connAccount;
|
||||
const result = await pool
|
||||
.request()
|
||||
.input("Email", sql.VarChar, email)
|
||||
.input("VerificationCode", sql.VarChar, verificationCode)
|
||||
.input("VerificationCodeType", sql.VarChar, verificationCodeType)
|
||||
.execute("GetVerificationCode");
|
||||
const row = result.recordset[0];
|
||||
|
||||
return row.Result;
|
||||
}
|
||||
|
||||
export async function clearVerificationCode(email) {
|
||||
const pool = connAccount;
|
||||
await pool
|
||||
.request()
|
||||
.input("Email", sql.VarChar, email)
|
||||
.execute("ClearVerificationCode");
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Billing Functions
|
||||
// ==============================================
|
||||
|
||||
export async function getCurrency(userId) {
|
||||
const pool = connAccount;
|
||||
const result = await pool
|
||||
.request()
|
||||
.input("UserId", sql.VarChar(50), userId)
|
||||
.input("ServerId", sql.Int, config.serverId)
|
||||
.execute("GetCurrency");
|
||||
|
||||
const row = result.recordset[0];
|
||||
if (!row || row.Result !== "Success") {
|
||||
throw new Error(row?.Result || "Failed to get balance");
|
||||
}
|
||||
return row.Zen;
|
||||
}
|
||||
|
||||
export async function setCurrency(userId, newBalance) {
|
||||
const pool = connAccount;
|
||||
const result = await pool
|
||||
.request()
|
||||
.input("UserId", sql.VarChar(50), userId)
|
||||
.input("ServerId", sql.Int, config.serverId)
|
||||
.input("NewBalance", sql.BigInt, newBalance)
|
||||
.execute("SetCurrency");
|
||||
|
||||
if (result.rowsAffected[0] === 0) {
|
||||
throw new Error("Balance update failed");
|
||||
}
|
||||
}
|
||||
|
||||
export async function logBillingTransaction(transaction) {
|
||||
const pool = connAccount;
|
||||
await pool
|
||||
.request()
|
||||
.input("userid", sql.VarChar(50), transaction.userId)
|
||||
.input("charid", sql.VarChar(50), transaction.charId)
|
||||
.input("uniqueid", sql.VarChar(50), transaction.uniqueId)
|
||||
.input("amount", sql.BigInt, transaction.amount)
|
||||
.input("itemid", sql.VarChar(50), transaction.itemId)
|
||||
.input("itemcount", sql.Int, transaction.count)
|
||||
.execute("SetBillingLog");
|
||||
}
|
||||
20
src/services/authDBService.js
Normal file
20
src/services/authDBService.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import sql from "mssql";
|
||||
import configs from "../config.js";
|
||||
const { config } = configs;
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
// ==============================================
|
||||
// Fetch online count from the database
|
||||
// ==============================================
|
||||
export async function fetchOnlineCount(pool) {
|
||||
try {
|
||||
const QUERY = 'SELECT COUNT(*) AS OnlineCount FROM AuthTable WHERE online = @online';
|
||||
const request = pool.request();
|
||||
request.input('online', sql.Int, 1);
|
||||
const result = await request.query(QUERY);
|
||||
return result.recordset[0].OnlineCount;
|
||||
} catch (error) {
|
||||
logger.error('Online count query failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
const sql = require('mssql');
|
||||
const env = require('./env');
|
||||
const { logger } = require('../utils/logger');
|
||||
import sql from 'mssql';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const dbConfig = {
|
||||
user: process.env.DB_USER,
|
||||
|
|
@ -32,7 +31,7 @@ const authDBConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
connAccount,
|
||||
authDBConfig
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
require('dotenv').config();
|
||||
|
||||
// Access environment variables with a function
|
||||
module.exports = {
|
||||
get: (key) => process.env[key],
|
||||
};
|
||||
36
src/utils/getClientIp.js
Normal file
36
src/utils/getClientIp.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import config from "../config.js";
|
||||
const { apiConfig } = config;
|
||||
|
||||
export function getClientIp(req) {
|
||||
let ip;
|
||||
|
||||
if (apiConfig.trustProxyEnabled) {
|
||||
const forwarded = req.headers["x-forwarded-for"];
|
||||
if (forwarded) {
|
||||
// Grab the first IP from x-forwarded-for list
|
||||
ip = forwarded.split(",")[0].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to connection-based IP
|
||||
if (!ip) {
|
||||
ip = req.socket?.remoteAddress || req.connection?.remoteAddress || null;
|
||||
}
|
||||
|
||||
// Optional fallback (only if explicitly included in body)
|
||||
if (!ip && req.body?.ip) {
|
||||
ip = req.body.ip;
|
||||
}
|
||||
|
||||
// Normalize IPv6 localhost
|
||||
if (ip === "::1" || ip === "0:0:0:0:0:0:0:1") {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
|
||||
// Remove IPv6 prefix if needed (e.g. "::ffff:192.168.0.1")
|
||||
if (ip && ip.startsWith("::ffff:")) {
|
||||
ip = ip.substring(7);
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
12
src/utils/hashUtils.js
Normal file
12
src/utils/hashUtils.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { createHash } from 'crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export function generateMD5Hash(account, password) {
|
||||
return createHash('md5').update(account + password).digest('hex');
|
||||
}
|
||||
export async function hashPassword(password, saltRounds = 10) {
|
||||
return await bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
export async function comparePassword(password, hash) {
|
||||
return await bcrypt.compare(password, hash);
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
const fs = require("fs");
|
||||
const winston = require("winston");
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { transports as _transports, format as _format, createLogger as _createLogger } from "winston";
|
||||
|
||||
const logsDirectory = 'logs';
|
||||
|
||||
if (!fs.existsSync(logsDirectory)) {
|
||||
fs.mkdirSync(logsDirectory);
|
||||
if (!existsSync(logsDirectory)) {
|
||||
mkdirSync(logsDirectory);
|
||||
}
|
||||
|
||||
function createLogger(filename, level, filter, showConsole) {
|
||||
const transports = [
|
||||
new winston.transports.File({
|
||||
new _transports.File({
|
||||
filename: `${logsDirectory}/${filename}-${new Date().toISOString().slice(0, 10)}.log`,
|
||||
level,
|
||||
filter
|
||||
|
|
@ -17,20 +19,20 @@ function createLogger(filename, level, filter, showConsole) {
|
|||
];
|
||||
|
||||
if (showConsole) {
|
||||
transports.push(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||
transports.push(new _transports.Console({
|
||||
format: _format.combine(
|
||||
_format.colorize(),
|
||||
_format.simple(),
|
||||
_format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
const logger = _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}] `)
|
||||
format: _format.combine(
|
||||
_format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
_format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||
),
|
||||
transports
|
||||
});
|
||||
|
|
@ -46,7 +48,7 @@ const mailerLogger = createLogger('mailer', logLevel, log => log.message.include
|
|||
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 = {
|
||||
export {
|
||||
authLogger,
|
||||
billingLogger,
|
||||
mailerLogger,
|
||||
|
|
|
|||
31
src/utils/memoryLogger.js
Normal file
31
src/utils/memoryLogger.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import moment from 'moment-timezone';
|
||||
import { formatBytes } from './systemInfo.js';
|
||||
|
||||
function setupMemoryLogging(interval = 1800000) { // 30 minutes
|
||||
// Set up periodic logging
|
||||
const intervalId = setInterval(logMemoryUsage, interval);
|
||||
|
||||
// Return function to stop logging
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
console.log('Stopped memory logging');
|
||||
};
|
||||
}
|
||||
|
||||
function logMemoryUsage() {
|
||||
const now = moment().format('YYYY-MM-DD HH:mm:ss');
|
||||
const mem = process.memoryUsage();
|
||||
|
||||
console.log(`Memory Usage at ${now}:`);
|
||||
console.log(` RSS : ${formatBytes(mem.rss)}`);
|
||||
console.log(` Heap Total : ${formatBytes(mem.heapTotal)}`);
|
||||
console.log(` Heap Used : ${formatBytes(mem.heapUsed)}`);
|
||||
console.log(` External : ${formatBytes(mem.external)}`);
|
||||
console.log(` Array Buffers: ${formatBytes(mem.arrayBuffers)}`);
|
||||
console.log('--------------------------------------------------');
|
||||
}
|
||||
|
||||
export default {
|
||||
setupMemoryLogging,
|
||||
logMemoryUsage
|
||||
};
|
||||
61
src/utils/systemInfo.js
Normal file
61
src/utils/systemInfo.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import tz from 'moment-timezone';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
const data = await fs.readFile(new URL('../../package.json', import.meta.url), 'utf-8');
|
||||
const { version: APP_VERSION } = JSON.parse(data);
|
||||
|
||||
function formatBytes(bytes) {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
if (bytes === 0) return '0 B';
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
||||
}
|
||||
|
||||
function getSystemInfo() {
|
||||
const nodeVersion = process.version;
|
||||
const timezone = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const offsetInMinutes = tz.tz(timezone).utcOffset();
|
||||
const offsetSign = offsetInMinutes >= 0 ? '+' : '-';
|
||||
const offsetHours = Math.floor(Math.abs(offsetInMinutes) / 60).toString().padStart(2, '0');
|
||||
const offsetMinutes = (Math.abs(offsetInMinutes) % 60).toString().padStart(2, '0');
|
||||
const offsetString = `${offsetSign}${offsetHours}:${offsetMinutes}`;
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const githubLink = 'https://github.com/JuniorDark/RustyHearts-API';
|
||||
|
||||
return {
|
||||
version: `Rusty Hearts API Version: ${APP_VERSION}`,
|
||||
github: `Github: ${githubLink}`,
|
||||
nodeVersion: `Node.js Version: ${nodeVersion}`,
|
||||
timezone: `Timezone: ${timezone} (${offsetString})`,
|
||||
memory: {
|
||||
rss: formatBytes(memoryUsage.rss),
|
||||
heapTotal: formatBytes(memoryUsage.heapTotal),
|
||||
heapUsed: formatBytes(memoryUsage.heapUsed),
|
||||
external: formatBytes(memoryUsage.external),
|
||||
arrayBuffers: formatBytes(memoryUsage.arrayBuffers)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function logSystemInfo() {
|
||||
const info = getSystemInfo();
|
||||
|
||||
console.log('--------------------------------------------------');
|
||||
console.log(info.version);
|
||||
console.log(info.github);
|
||||
console.log(info.nodeVersion);
|
||||
console.log(info.timezone);
|
||||
console.log('Memory Usage:');
|
||||
console.log(` RSS : ${info.memory.rss}`);
|
||||
console.log(` Heap Total : ${info.memory.heapTotal}`);
|
||||
console.log(` Heap Used : ${info.memory.heapUsed}`);
|
||||
console.log(` External : ${info.memory.external}`);
|
||||
console.log(` Array Buffers: ${info.memory.arrayBuffers}`);
|
||||
console.log('--------------------------------------------------');
|
||||
}
|
||||
|
||||
export {
|
||||
getSystemInfo,
|
||||
logSystemInfo,
|
||||
formatBytes
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue