Add project files.

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

83
.env Normal file
View file

@ -0,0 +1,83 @@
##################################
# API CONFIGURATION
##################################
# Set the port for receiving connections
PORT=3000
AUTH_PORT=8070
BILLING_PORT=8080
Determines whether the helmet middleware is enabled or disabled. If enabled https need to be used for the api.
If set to true, the helmet middleware is included in the middleware stack, which adds various security-related HTTP headers to the application's responses to help prevent common web vulnerabilities.
If set to false, the helmet middleware is not included in the middleware stack, and the application's responses will not have these extra headers.
ENABLE_HELMET=false
# Set the server timezone
TZ=America/New_York
##################################
# LOGGING CONFIGURATION #
##################################
LOG_LEVEL=info
LOG_AUTH_CONSOLE=true
LOG_BILLING_CONSOLE=true
LOG_ACCOUNT_CONSOLE=false
LOG_MAILER_CONSOLE=false
##################################
# API DATABASE CONFIGURATION #
##################################
# Set a host to connect to the SQL server database.
DB_SERVER=127.0.0.1
# Set the name of database
DB_DATABASE=RustyHearts_Account
# Set the user to connect to database
DB_USER=sa
# Set the password to connect to database
DB_PASSWORD=
Set to encrypt the connection to the database
DB_ENCRYPT=false
##################################
# GATEWAY API CONFIGURATION #
##################################
# Set the host for receiving connections to the gateserver
GATESERVER_IP=YOUR_SERVER_IP
# Set the port for receiving connections to the gateserver
GATESERVER_PORT=50001
##################################
# EMAIL CONFIGURATION #
##################################
using gmail smtp server
# To generate app passwords, first you have to enable 2-Step Verification on our Google account.
# Go to your Google account security settings (https://myaccount.google.com/security) and enable 2-Step Verification
# Now, you can select the App passwords option to set up a new app password. https://myaccount.google.com/u/2/apppasswords
The hostname or IP address of the SMTP server
SMTP_HOST=smtp.gmail.com
The port number of the SMTP server
SMTP_PORT=465
The encryption protocol to use (e.g. ssl, tls)
SMTP_ENCRYPTION=ssl
# your gmail
SMTP_USERNAME=your.email@gmail.com
# app password
SMTP_PASSWORD=
The name to use as the sender in emails
SMTP_FROMNAME=Rusty Hearts

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [JuniorDark]

13
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,13 @@
# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json
version: 2
updates:
- directory: /
package-ecosystem: github-actions
schedule:
interval: weekly
labels: []
- directory: /
package-ecosystem: npm
schedule:
interval: weekly
labels: []

130
.gitignore vendored Normal file
View file

@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
#.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

12
LICENSE Normal file
View file

@ -0,0 +1,12 @@
BSD Zero Clause License (0BSD)
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

150
README.md Normal file
View file

@ -0,0 +1,150 @@
# RustyHearts-API
[![License](https://img.shields.io/github/license/JuniorDark/RustyHearts-API?color=brown)](LICENSE)
RustyHearts-API is a Node.js-based REST API that enables authentication, billing, and launcher functionalities for Rusty Hearts.
The API consists of three independent servers (Auth API, Billing API and Launcher API) running on different ports.
### API region support
The api currently only support the **usa** (PWE) region.
* **usa** (PWE) - Full api support
* **chn** (Xunlei) - Only launcher support
## Server Descriptions
- The Auth API is responsible for in-game authentication, while the Billing API manages the shop's zen balance and purchases. It is essential to bind the Auth/Billing API only to a local IP address and prevent external access to these APIs.
- The Launcher API is a web server intended to handle the client connection to the gateserver and for the [Rusty Hearts Launcher](https://github.com/JuniorDark/RustyHearts-Launcher), which handles registration, login, client updates, and processing static elements (public directory). This API must be accessible from the outside and proxied by Nginx or bound to an external IP.
## Table of Contents
* [Public folder](#public-folder)
* [Requirements](#requirements)
* [Deployment](#deployment)
* [Basic Installation](#basic-installation)
* [.env file setup](#env-file-setup)
* [Available endpoints](#available-endpoints)
* [Preview](#preview)
* [License](#license)
* [Contributing](#contributing)
* [FAQ](#faq)
* [Support](#support)
## Public folder description
### Launcher self-update
In order for the launcher to automatically update itself, you need to use the launcher_info.ini in the `launcher_update` directory of the api. This file specifies the version of the launcher. After each update of the launcher, you need to change the version in the ini, as well in the launcher executable file.
### Client patch
In order to create client patches, you need to use the `patch` directory of the api.
The tool for creating the patch files is available in the repository: https://github.com/JuniorDark/RustyHearts-MIPTool
### News panel
Used to use the html page displayed in the launcher, uses the `news` directory of the api
## Requirements
Before deploying RustyHearts-API, ensure that you have the following software installed:
* [Node.js](https://nodejs.org/en/) version 18.5.0 or higher
* [Microsoft SQL Server](https://go.microsoft.com/fwlink/p/?linkid=2215158) version 2019 or 2022 Developer edition
* [Rusty Hearts Retail Server](https://forum.ragezone.com)
## Deployment
To deploy RustyHearts-API, follow these steps:
### Basic Installation
1. Install the latest version of Node.js from the [official website](https://nodejs.org/).
2. Copy all RustyHearts-API files to a directory of your choice (e.g., **c:\RustyHearts-API**).
3. Open a terminal window, navigate to the RustyHearts-API directory, and execute the `npm install` command. Alternatively, you can run the **install.bat** file.
4. Import the [database file](share/RustyHearts_Account.sql) to your Microsoft SQL Server.
5. Configure the parameters in the **.env** file.
6. Start RustyHearts-API servers by executing the `node src/app` command or running the **rh-api.bat** file.
7. The server region must be set to **usa** on [service_control.xml](share/service_control.xml)
## .env file setup:
### API CONFIGURATION
- **PORT**: The port number for receiving connections (default 3000).
- **AUTH_PORT**: The port number for the Auth API.
- **BILLING_PORT**: The port number for the Billing API.
- **ENABLE_HELMET**: Determines whether the helmet middleware is enabled or disabled. If enabled, https need to be used for the api.
- **TZ**: The timezone for the server.
### LOGGING CONFIGURATION
- **LOG_LEVEL**: The level of logging to use (e.g. debug, info, warn, error).
- **LOG_AUTH_CONSOLE**: Whether to log Auth API messages to the console.
- **LOG_BILLING_CONSOLE**: Whether to log Billing API messages to the console.
- **LOG_ACCOUNT_CONSOLE**: Whether to log Account API messages to the console.
- **LOG_MAILER_CONSOLE**: Whether to log email messages to the console.
### DATABASE CONFIGURATION
- **DB_SERVER**: The IP address or hostname of the SQL Server.
- **DB_DATABASE**: The name of the database to connect to (RustyHearts_Account).
- **DB_USER**: The user to connect to the database.
- **DB_PASSWORD**: The password for the database user.
- **DB_ENCRYPT**: Whether to encrypt the connection to the database.
### GATEWAY API CONFIGURATION
- **GATESERVER_IP**: The IP address of the gate server.
- **GATESERVER_PORT**: The port number of the gate server.
### EMAIL CONFIGURATION
- **SMTP_HOST**: The hostname or IP address of the SMTP server.
- **SMTP_PORT**: The port number of the SMTP server.
- **SMTP_ENCRYPTION**: The encryption protocol to use (e.g. ssl, tls).
- **SMTP_USERNAME**: The username for the SMTP server.
- **SMTP_PASSWORD**: The password for the SMTP server.
- **SMTP_FROMNAME**: The name to use as the sender in emails.
## Available endpoints
The api provides the following endpoints:
Endpoint | Method | Arguments | Description
--- | --- | --- | ---
/serverApi/auth | POST | XML with account, password, game and IP | Authenticates a user based on their account information and sends an XML response with their user ID, user type, and success status. If authentication fails, it sends an XML response with a failure status.
/serverApi/billing | POST | XML with currency-request or item-purchase-request and associated arguments | Handles billing requests. For currency requests, it retrieves the user's Zen balance from the database and sends an XML response with the balance. For item purchase requests, it deducts the cost of the item from the user's Zen balance and logs the transaction in the database. If the transaction is successful, it sends an XML response with the success status. If the transaction fails, it sends an XML response with a failure status and an error message.
/serverApi/gateway | GET | | Returns an XML response containing the IP address and port number of the gateway server.
/serverApi/gateway/status | GET | | Checks the status of the gateway server by attempting to establish a connection to the server. Returns a JSON object with the status of the server (online or offline) and an HTTP status code indicating the success or failure of the connection attempt.
/accountApi/register | POST | windyCode, email, password | Create a new account with the provided windyCode, email, and password. The password is first combined with the windyCode to create an MD5 hash, which is then salted and hashed again using bcrypt before being stored in the database. An email confirmation is sent to the provided email address, and a success or error message is returned.
/accountApi/login | POST | account, password | Authenticates a user account by username or email address and password. Return a token if the authentication is successful (unsued).
/accountApi/codeVerification | POST | email, verification_code_type, verification_code | Verify a user's email by checking the verification code
/accountApi/sendPasswordResetEmail | POST | email | Sends an email with a password reset verification code to the specified email address
/accountApi/changePassword | POST | email, password, verification_code | Change the password of a user's account, given the email and password verification code
/accountApi/sendVerificationEmail | POST | email | Sends a verification email to the specified email address.
/launcherApi/launcherUpdater/getLauncherVersion | GET | | Returns the version of the launcher by reading the launcher_info.ini file.
/launcherApi/launcherUpdater/updateLauncherVersion | POST | version | Downloads the new version of the launcher from the launcher_update folder.
### Preview
![image](api.png)
## License
This project is licensed under the terms found in [`LICENSE-0BSD`](LICENSE).
## Contributing
Contributions from the community are welcome! If you encounter a bug or have a feature request, please submit an issue on GitHub. If you would like to contribute code, please fork the repository and submit a pull request.
## FAQ
* Q: How do I report a bug?
* A: Please submit an issue on GitHub with a detailed description of the bug and steps to reproduce it.
* Q: How do I request a new feature?
* A: Please submit an issue on GitHub with a detailed description of the feature and why it would be useful.
* Q: How do I contribute code?
* A: Please fork the repository, make your changes, and submit a pull request.
## Support
If you need help with the api, please submit an issue on GitHub.
## Roadmap
* Add support for client download/repair
* Improve performance and stability
* Add support for other regions

BIN
api.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

4
install.bat Normal file
View file

@ -0,0 +1,4 @@
@echo off
cd .
npm install
pause

46
package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "RustyHearts-API",
"version": "1.0.0",
"description": "Rusty Hearts REST API implementation on node.js",
"main": "src/app.js",
"scripts": {
"start": "node src/app"
},
"author": "JuniorDark",
"keywords": [
"api",
"rustyhearts",
"launcher"
],
"homepage": "https://github.com/JuniorDark/RustyHearts-API",
"repository": {
"type": "git",
"url": "https://github.com/JuniorDark/RustyHearts-API/RustyHearts-API.git"
},
"bugs": {
"url": "https://github.com/JuniorDark/RustyHearts-API/issues"
},
"dependencies": {
"bcrypt": "^5.1.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"express-validator": "^7.0.1",
"express-winston": "^4.2.0",
"handlebars": "^4.7.7",
"helmet": "^7.0.0",
"joi": "^17.9.2",
"logger": "^0.0.1",
"moment-timezone": "^0.5.43",
"mssql": "^9.1.1",
"nodemailer": "^6.9.1",
"winston": "^3.8.2",
"xml2js": "^0.5.0"
},
"engines": {
"node": ">=18.15.0"
},
"license": "0BSD"
}

View file

@ -0,0 +1,2 @@
[LAUNCHER]
version=1.0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>News Panel</title>
<link rel="stylesheet" href="/launcher/news/style.css">
</head>
<body oncontextmenu="return false;">
<div class="slider-container">
<div class="slider">
<img src="/launcher/news/images/lb_cashshop_banner07.png?text=Halloween" alt="Halloween" onclick="location.href='https://your-website.com/news/Halloween.html';">
<img src="/launcher/news/images/lb_cashshop_banner08.png?text=Winter" alt="Winter" onclick="location.href='https://your-website.com/news/Winter.html';">
<img src="/launcher/news/images/lb_cashshop_banner09.png?text=Happy New Year" alt="Happy New Year" onclick="location.href='https://your-website.com/news/Happy_New_Year.html';">
</div>
<div class="slider-arrow slider-arrow-left">&lt;</div>
<div class="slider-arrow slider-arrow-right">&gt;</div>
<div class="slider-dots">
<div class="slider-dot active"></div>
<div class="slider-dot"></div>
<div class="slider-dot"></div>
</div>
</div>
<div class="tab-links">
<div class="tab-link active" data-tab="events">Events</div>
<div class="tab-link" data-tab="notices">Notices</div>
<div class="tab-link" data-tab="info">Info</div>
</div>
<div class="tab-content tab events active">
<a href="https://your-website.com/news/Halloween.html">Halloween</a> - <span class="tab-date">20/10/2023</span><br>
<a href="https://your-website.com/news/Winter.html">Winter</a> - <span class="tab-date">10/12/2023</span><br>
<a href="https://your-website.com/news/Happy_New_Year.html">Happy New Year</a> - <span class="tab-date">01/01/2024</span><br>
</div>
<div class="tab-content tab notices">
<a href="#">Notice 1</a> - <span class="tab-date">01/01/2023</span><br>
<a href="#">Notice 2</a> - <span class="tab-date">02/01/2023</span><br>
<a href="#">Notice 3</a> - <span class="tab-date">03/01/2023</span><br>
</div>
<div class="tab-content tab info">
<a href="#">Info 1</a> - <span class="tab-date">01/01/2023</span><br>
<a href="#">Info 2</a> - <span class="tab-date">02/01/2023</span><br>
<a href="#">Info 3</a> - <span class="tab-date">03/01/2023</span><br>
</div>
<script src="/launcher/news/script.js"></script>
</body>
</html>

View file

@ -0,0 +1,91 @@
/* Slider */
const slider = document.querySelector('.slider');
const slides = slider.querySelectorAll('img');
const arrowLeft = document.querySelector('.slider-arrow-left');
const arrowRight = document.querySelector('.slider-arrow-right');
const dots = document.querySelectorAll('.slider-dot');
let currentSlide = 0;
let interval;
function showSlide(index) {
slides.forEach(slide => slide.style.transform = `translateX(-${index * 100}%)`);
dots.forEach(dot => dot.classList.remove('active'));
dots[index].classList.add('active');
currentSlide = index;
}
function nextSlide() {
currentSlide++;
if (currentSlide >= slides.length) {
currentSlide = 0;
}
showSlide(currentSlide);
}
function prevSlide() {
currentSlide--;
if (currentSlide < 0) {
currentSlide = slides.length - 1;
}
showSlide(currentSlide);
}
function startInterval() {
interval = setInterval(() => {
nextSlide();
}, 5000);
}
function stopInterval() {
clearInterval(interval);
}
arrowLeft.addEventListener('click', () => {
stopInterval();
prevSlide();
startInterval();
});
arrowRight.addEventListener('click', () => {
stopInterval();
nextSlide();
startInterval();
});
dots.forEach((dot, index) => {
dot.addEventListener('click', () => {
stopInterval();
showSlide(index);
startInterval();
});
});
slider.addEventListener('mouseenter', () => {
arrowLeft.style.display = 'block';
arrowRight.style.display = 'block';
});
slider.addEventListener('mouseleave', () => {
arrowLeft.style.display = 'none';
arrowRight.style.display = 'none';
});
startInterval();
/* Tabs */
const tabLinks = document.querySelectorAll('.tab-link');
const tabs = document.querySelectorAll('.tab');
function showTab(tab) {
tabs.forEach(tab => tab.classList.remove('active'));
tab.classList.add('active');
}
tabLinks.forEach(link => {
link.addEventListener('click', () => {
const tab = document.querySelector(`.tab.${link.dataset.tab}`);
showTab(tab);
tabLinks.forEach(link => link.classList.remove('active'));
link.classList.add('active');
});
});

View file

@ -0,0 +1,135 @@
body {
background-color: #151d4c;
}
/* Slider styles */
.slider-container {
position: relative;
width: 460px;
height: 214px;
overflow: hidden;
margin-bottom: 0;
}
.slider {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
transition: all 0.5s ease;
}
.slider img {
width: 100%;
height: 100%;
object-fit: cover;
}
.slider-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 400px;
background-color: rgb(0 0 0 / 75%);
color: #fff;
font-size: 24px;
font-weight: 700;
text-align: center;
line-height: 400px;
cursor: pointer;
transition: all 0.3s ease;
display: none;
}
.slider-arrow:hover {
background-color: rgba(0, 0, 0, 0.5);
}
.slider-arrow-left {
left: 0;
}
.slider-arrow-right {
right: 0;
}
.slider-dots {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
}
.slider-dot {
width: 15px;
height: 15px;
border-radius: 50%;
background-color: rgb(68 66 66);
margin: -8px 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.slider-dot.active {
background-color: rgb(255 255 255);
}
/* Tab styles */
.tab {
display: none;
width: 460px;
height: 100px;
padding: 10px;
border: 1px solid #ccc;
box-sizing: border-box;
}
.tab.active {
display: block;
}
.tab-links {
display: flex;
width: 460px;
margin-bottom: 10px;
margin-top: 20px;
background-color: transparent;
border-top: none;
}
.tab-link {
flex: 1;
text-align: center;
color: #e5e6eb;
background-color: #151d4c;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.tab-link.active {
background-color: #ffffff;
border-bottom: 1px solid #ccc;
color: #151d4c;
}
.tab-content {
font-size: 15px;
line-height: 1.5;
margin-bottom: 10px;
background-color: #ffffffe3;
}
.tab-date {
font-size: 12px;
color: #000;
text-align: right;
}
.slider-container:hover .slider-arrow {
display: block !important;
}

4
rh-api.bat Normal file
View file

@ -0,0 +1,4 @@
@echo off
title Rusty Hearts API
node src/app
pause

View file

@ -0,0 +1,755 @@
/*
Navicat Premium Data Transfer
Source Server : RH VM
Source Server Type : SQL Server
Source Server Version : 16001050
Source Host : 192.168.100.125:1433
Source Catalog : RustyHearts_Account
Source Schema : dbo
Target Server Type : SQL Server
Target Server Version : 16001050
File Encoding : 65001
Date: 12/05/2023 14:59:51
*/
-- ----------------------------
-- Table structure for AccountTable
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[AccountTable]') AND type IN ('U'))
DROP TABLE [dbo].[AccountTable]
GO
CREATE TABLE [dbo].[AccountTable] (
[AccountID] int IDENTITY(1,1) NOT NULL,
[WindyCode] varchar(50) COLLATE Chinese_PRC_CI_AS NOT NULL,
[AccountPwd] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL,
[Email] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL,
[RegisterIP] varchar(16) COLLATE Chinese_PRC_CI_AS NOT NULL,
[CreatedAt] datetime DEFAULT getdate() NOT NULL,
[LastLogin] datetime DEFAULT getdate() NOT NULL,
[IsLocked] bit NOT NULL,
[LoginAttempts] int NOT NULL,
[LastLoginIP] varchar(16) COLLATE Chinese_PRC_CI_AS NOT NULL,
[Token] varchar(255) COLLATE Chinese_PRC_CI_AS NULL
)
GO
ALTER TABLE [dbo].[AccountTable] SET (LOCK_ESCALATION = TABLE)
GO
-- ----------------------------
-- Table structure for BillingLog
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[BillingLog]') AND type IN ('U'))
DROP TABLE [dbo].[BillingLog]
GO
CREATE TABLE [dbo].[BillingLog] (
[bid] int IDENTITY(1,1) NOT NULL,
[BuyTime] datetime NULL,
[WindyCode] varchar(50) COLLATE Chinese_PRC_CI_AS NULL,
[CharId] varchar(128) COLLATE Chinese_PRC_CI_AS NULL,
[UniqueId] varchar(128) COLLATE Chinese_PRC_CI_AS NULL,
[Amount] int NULL,
[ItemId] int NULL,
[ItemCount] int NULL
)
GO
ALTER TABLE [dbo].[BillingLog] SET (LOCK_ESCALATION = TABLE)
GO
-- ----------------------------
-- Table structure for CashTable
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[CashTable]') AND type IN ('U'))
DROP TABLE [dbo].[CashTable]
GO
CREATE TABLE [dbo].[CashTable] (
[WindyCode] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL,
[WorldId] int NULL,
[Zen] bigint NULL
)
GO
ALTER TABLE [dbo].[CashTable] SET (LOCK_ESCALATION = TABLE)
GO
-- ----------------------------
-- Table structure for VerificationCode
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[VerificationCode]') AND type IN ('U'))
DROP TABLE [dbo].[VerificationCode]
GO
CREATE TABLE [dbo].[VerificationCode] (
[id] int IDENTITY(1,1) NOT NULL,
[Email] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL,
[VerificationCode] varchar(10) COLLATE Chinese_PRC_CI_AS NOT NULL,
[ExpirationTime] datetime NOT NULL,
[Type] varchar(20) COLLATE Chinese_PRC_CI_AS NOT NULL
)
GO
ALTER TABLE [dbo].[VerificationCode] SET (LOCK_ESCALATION = TABLE)
GO
-- ----------------------------
-- procedure structure for GetVerificationCode
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[GetVerificationCode]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[GetVerificationCode]
GO
CREATE PROCEDURE [dbo].[GetVerificationCode]
@VerificationCode varchar(10),
@Email varchar(255),
@VerificationCodeType varchar(20)
AS
BEGIN
DECLARE @Result varchar(30)
DECLARE @ExpirationTime DATETIME
DECLARE @Now DATETIME = GETDATE()
DECLARE @VerificationCodeExists int;
SELECT @VerificationCodeExists = COUNT(*) FROM VerificationCode
WHERE Email = @Email AND VerificationCode = @VerificationCode AND Type = @VerificationCodeType
-- Check if VerificationCode exists
IF @VerificationCodeExists > 0
SET @Result = 'VerificationCodeExists';
ELSE
SET @Result = 'InvalidVerificationCode';
SELECT @ExpirationTime = ExpirationTime
FROM VerificationCode
WHERE Email = @Email AND VerificationCode = @VerificationCode
IF @Result = 'VerificationCodeExists'
BEGIN
IF @ExpirationTime > @Now
SET @Result = 'ValidVerificationCode';
ELSE
SET @Result = 'ExpiredVerificationCode';
END
SELECT @Result as Result;
END
GO
-- ----------------------------
-- procedure structure for UpdateAccountPassword
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[UpdateAccountPassword]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[UpdateAccountPassword]
GO
CREATE PROCEDURE [dbo].[UpdateAccountPassword]
@AccountPwd varchar(255),
@Email varchar(255)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Result varchar(20)
DECLARE @AccountExists int;
BEGIN TRY
BEGIN TRANSACTION
SELECT @AccountExists = COUNT(*) FROM AccountTable
WHERE Email = @Email;
-- Check if account exists
IF @AccountExists > 0
SET @Result = 'AccountExists';
ELSE
SET @Result = 'Failed';
-- Update password
IF @Result = 'AccountExists'
BEGIN
UPDATE AccountTable SET AccountPwd = @AccountPwd
WHERE Email = @Email;
SET @Result = 'PasswordChanged';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
END CATCH
SELECT @Result as Result;
END
GO
-- ----------------------------
-- procedure structure for ClearVerificationCode
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[ClearVerificationCode]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[ClearVerificationCode]
GO
CREATE PROCEDURE [dbo].[ClearVerificationCode]
@Email varchar(255)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Result varchar(30)
DECLARE @VerificationCodeExists int;
BEGIN TRY
BEGIN TRANSACTION
SELECT @VerificationCodeExists = COUNT(*) FROM VerificationCode
WHERE Email = @Email;
-- Check if VerificationCode exists
IF @VerificationCodeExists > 0
SET @Result = 'VerificationCodeExists';
ELSE
SET @Result = 'NoVerificationCode';
-- DELETE VerificationCodes
IF @Result = 'VerificationCodeExists'
BEGIN
DELETE FROM VerificationCode WHERE Email = @Email;
SET @Result = 'VerificationCodeClean';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
END CATCH
SELECT @Result as Result;
END
GO
-- ----------------------------
-- procedure structure for GetAccount
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[GetAccount]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[GetAccount]
GO
CREATE PROCEDURE [dbo].[GetAccount]
@Identifier varchar(255)
AS
BEGIN
DECLARE @Result varchar(20)
DECLARE @AccountExists int;
DECLARE @WindyCode varchar(50)
DECLARE @AccountPwd varchar(255)
SELECT @AccountExists = COUNT(*) FROM AccountTable
WHERE Email = @Identifier OR WindyCode = @Identifier;
SELECT @WindyCode = WindyCode FROM AccountTable
WHERE Email = @Identifier OR WindyCode = @Identifier;
SELECT @AccountPwd = AccountPwd FROM AccountTable
WHERE Email = @Identifier OR WindyCode = @Identifier;
-- Check if account exists
IF @AccountExists > 0
SET @Result = 'AccountExists';
ELSE
SET @Result = 'AccountNotFound';
SELECT @Result as Result, @WindyCode as WindyCode, @AccountPwd as AccountPwd;
END
GO
-- ----------------------------
-- procedure structure for GetCurrency
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[GetCurrency]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[GetCurrency]
GO
CREATE PROCEDURE [dbo].[GetCurrency]
@UserId varchar(50),
@ServerId int
AS
BEGIN
SET NOCOUNT ON
DECLARE @Result varchar(20)
DECLARE @Zen int;
BEGIN TRY
BEGIN TRANSACTION
-- Check if entry with given UserId and ServerId exists
SELECT @Zen = Zen FROM CashTable
WHERE WindyCode = @UserId AND WorldId = @ServerId;
IF @@ROWCOUNT > 0 -- entry exists
BEGIN
SET @Result = 'Success';
END
ELSE -- entry does not exist, insert new one
BEGIN
INSERT INTO CashTable (WindyCode, WorldId, Zen)
VALUES (@UserId, @ServerId, 0);
SET @Result = 'Success';
SET @Zen = 0;
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
SET @Zen = 0;
END CATCH
SELECT @Result as Result, @Zen as Zen;
END
GO
-- ----------------------------
-- procedure structure for SetPasswordVerificationCode
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[SetPasswordVerificationCode]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[SetPasswordVerificationCode]
GO
CREATE PROCEDURE [dbo].[SetPasswordVerificationCode]
@VerificationCode varchar(10),
@Email varchar(255),
@ExpirationTime DATETIME
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Result varchar(20);
DECLARE @VerificationCodeCount int;
BEGIN TRY
BEGIN TRANSACTION;
-- Retrieve count of existing verification codes for the user
SELECT @VerificationCodeCount = COUNT(*) FROM VerificationCode
WHERE Email = @Email;
-- Check if count of existing verification codes is less than 5
IF @VerificationCodeCount < 5
BEGIN
-- Insert new verification code
INSERT INTO VerificationCode (VerificationCode, Email, ExpirationTime, Type)
VALUES (@VerificationCode, @Email, @ExpirationTime, 'Password');
SET @Result = 'Success';
END
ELSE
BEGIN
-- Delete all existing verification codes for the user
DELETE FROM VerificationCode WHERE Email = @Email;
-- Insert new verification code
INSERT INTO VerificationCode (VerificationCode, Email, ExpirationTime, Type)
VALUES (@VerificationCode, @Email, @ExpirationTime, 'Password');
SET @Result = 'Success';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
END CATCH;
SELECT @Result as Result;
END;
GO
-- ----------------------------
-- procedure structure for SetCurrency
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[SetCurrency]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[SetCurrency]
GO
CREATE PROCEDURE [dbo].[SetCurrency]
@UserId varchar(50),
@ServerId int,
@NewBalance int
AS
BEGIN
SET NOCOUNT ON
DECLARE @Result varchar(20)
DECLARE @Zen int;
BEGIN TRY
BEGIN TRANSACTION
-- Check if entry with given UserId and ServerId exists
SELECT @Zen = Zen FROM CashTable
WHERE WindyCode = @UserId AND WorldId = @ServerId;
IF @@ROWCOUNT > 0 -- entry exists
BEGIN
UPDATE CashTable SET Zen = @NewBalance
WHERE WindyCode = @UserId AND WorldId = @ServerId;
SET @Zen = @NewBalance;
SET @Result = 'Success';
END
ELSE -- entry does not exist
BEGIN
SET @Result = 'Failed';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
END CATCH
SELECT @Result as Result, @Zen as Zen;
END
GO
-- ----------------------------
-- procedure structure for SetBillingLog
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[SetBillingLog]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[SetBillingLog]
GO
CREATE PROCEDURE [dbo].[SetBillingLog]
(
@userid varchar(50),
@charid varchar(128),
@uniqueid varchar(128),
@amount int,
@itemid int,
@itemcount int
)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
-- Insert the values into the BillingLog table
INSERT INTO BillingLog (BuyTime, WindyCode, CharId, UniqueId, Amount, ItemId, ItemCount)
VALUES (GETDATE(), @userid, @charid, @uniqueid, @amount, @itemid, @itemcount);
-- Return a success message
SELECT 'Success' AS Result;
END TRY
BEGIN CATCH
-- Log the error and return an error message
DECLARE @errorMessage varchar(4000) = ERROR_MESSAGE();
RAISERROR(@errorMessage, 16, 1);
-- Return an error message
SELECT 'Error: ' + @errorMessage AS Result;
END CATCH;
END;
GO
-- ----------------------------
-- procedure structure for CreateAccount
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[CreateAccount]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[CreateAccount]
GO
CREATE PROCEDURE [dbo].[CreateAccount]
@WindyCode varchar(50),
@AccountPwd varchar(255),
@Email varchar(255),
@RegisterIP varchar(16)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Result varchar(20)
DECLARE @AccountExists int;
DECLARE @WindyCodeExists int;
BEGIN TRY
BEGIN TRANSACTION
SELECT @AccountExists = COUNT(*) FROM AccountTable
WHERE WindyCode = @WindyCode OR Email = @Email;
SELECT @WindyCodeExists = COUNT(*) FROM RustyHearts_Auth.dbo.AuthTable
WHERE WindyCode = @WindyCode;
-- Check if account exists
IF @AccountExists > 0
SET @Result = 'AccountExists';
ELSE IF @WindyCodeExists > 0
SET @Result = 'WindyCodeExists';
ELSE
SET @Result = 'NewUser';
-- Create new account
IF @Result = 'NewUser'
BEGIN
INSERT INTO AccountTable (WindyCode, AccountPwd, Email, RegisterIP, CreatedAt, LastLogin, IsLocked, LoginAttempts, LastLoginIP)
VALUES (@WindyCode, @AccountPwd, @Email, @RegisterIP, GETDATE(), GETDATE(), 0, 0, @RegisterIP);
INSERT INTO RustyHearts_Auth.dbo.AuthTable (WindyCode, world_id, AuthID, Tcount, online, CTime, BTime, LTime, IP, LCount, ServerIP, ServerType, HostID, DBCIndex, InquiryCount, event_inquiry, CashMileage, channelling, pc_room_point, externcash, mac_addr, mac_addr02, mac_addr03, second_pass)
VALUES (@WindyCode, 0, NEWID(), 0, '0', GETDATE(), GETDATE(), GETDATE(), @RegisterIP, 0, 0, 0, 0, 0, 5, 1, 0, 1, 0, 0, '00-00-00-00-00-00', '00-00-00-00-00-00', '00-00-00-00-00-00', '');
INSERT INTO CashTable (WindyCode, WorldId, Zen)
VALUES (@WindyCode, 10101, 0);
SET @Result = 'AccountCreated';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
END CATCH
SELECT @Result as Result;
END
GO
-- ----------------------------
-- procedure structure for AuthenticateUser
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[AuthenticateUser]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[AuthenticateUser]
GO
CREATE PROCEDURE [dbo].[AuthenticateUser]
@Identifier varchar(255),
@password_verify_result BIT,
@LastLoginIP varchar(15)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Result varchar(20)
DECLARE @WindyCode varchar(50)
DECLARE @AuthID varchar(50)
DECLARE @LoginAttempts int
DECLARE @IsLocked BIT
DECLARE @Now datetime = GETDATE()
DECLARE @LastLogin datetime
DECLARE @Token NVARCHAR(64)
DECLARE @RandomBytes VARBINARY(32)
BEGIN TRY
BEGIN TRANSACTION
-- Retrieve account information
SELECT @WindyCode = WindyCode, @LoginAttempts = LoginAttempts, @IsLocked = IsLocked, @LastLogin = LastLogin
FROM AccountTable
WHERE WindyCode = @Identifier OR Email = @Identifier;
SELECT @AuthID = AuthID
FROM RustyHearts_Auth.dbo.AuthTable
WHERE WindyCode = @WindyCode;
-- Check if last login attempt is within 5 minutes
IF DATEDIFF(minute, @LastLogin, @Now) > 5
BEGIN
UPDATE AccountTable SET LoginAttempts = 0 WHERE WindyCode = @Identifier OR Email = @Identifier;
END
-- Verify password
IF @password_verify_result = 1
BEGIN
SET @Result = 'LoginSuccess';
SET @RandomBytes = CAST(CRYPT_GEN_RANDOM(32) AS VARBINARY(32)) -- Generate 32 random bytes
SET @Token = LOWER(CONVERT(NVARCHAR(64), HashBytes('SHA2_256', @RandomBytes), 2)) -- Hash the random bytes using SHA256 and convert to lowercase hexadecimal string
END
ELSE
SET @Result = 'InvalidCredentials';
-- Check account status
IF @Result = 'LoginSuccess' AND @IsLocked = 1
SET @Result = 'Locked';
ELSE IF @LoginAttempts >= 10
SET @Result = 'TooManyAttempts';
ELSE
-- Update login attempts, token, and last login IP
IF @Result = 'LoginSuccess'
BEGIN
UPDATE AccountTable SET LoginAttempts = 0, Token = @Token, LastLoginIP = @LastLoginIP, LastLogin = @Now WHERE (WindyCode = @Identifier OR Email = @Identifier);
END
ELSE IF @Result = 'InvalidCredentials'
BEGIN
UPDATE AccountTable SET LoginAttempts = @LoginAttempts + 1, LastLogin = @Now WHERE (WindyCode = @Identifier OR Email = @Identifier);
END
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
END CATCH
SELECT @Result as Result, @WindyCode as WindyCode, @AuthID as AuthID, @Token as Token;
END
GO
-- ----------------------------
-- procedure structure for SetAccountVerificationCode
-- ----------------------------
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[SetAccountVerificationCode]') AND type IN ('P', 'PC', 'RF', 'X'))
DROP PROCEDURE[dbo].[SetAccountVerificationCode]
GO
CREATE PROCEDURE [dbo].[SetAccountVerificationCode]
@VerificationCode varchar(10),
@Email varchar(255),
@ExpirationTime DATETIME
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Result varchar(20)
DECLARE @AccountExists int;
DECLARE @VerificationCodeCount int;
BEGIN TRY
BEGIN TRANSACTION
SELECT @AccountExists = COUNT(*) FROM AccountTable
WHERE Email = @Email;
-- Check if account exists
IF @AccountExists > 0
SET @Result = 'AccountExists';
ELSE
SET @Result = 'AccountDontExists';
IF @Result = 'AccountDontExists'
-- Retrieve count of existing verification codes for the user
SELECT @VerificationCodeCount = COUNT(*) FROM VerificationCode
WHERE Email = @Email;
-- Check if count of existing verification codes is less than 5
IF @VerificationCodeCount < 5
BEGIN
-- Insert new verification code
INSERT INTO VerificationCode (VerificationCode, Email, ExpirationTime, Type)
VALUES (@VerificationCode, @Email, @ExpirationTime, 'Account');
SET @Result = 'Success';
END
ELSE
BEGIN
-- Delete all existing verification codes for the user
DELETE FROM VerificationCode WHERE Email = @Email;
-- Insert new verification code
INSERT INTO VerificationCode (VerificationCode, Email, ExpirationTime, Type)
VALUES (@VerificationCode, @Email, @ExpirationTime, 'Account');
SET @Result = 'Success';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Result = 'TransactionFailed';
END CATCH
SELECT @Result as Result;
END
GO
-- ----------------------------
-- Auto increment value for AccountTable
-- ----------------------------
DBCC CHECKIDENT ('[dbo].[AccountTable]', RESEED, 2)
GO
-- ----------------------------
-- Primary Key structure for table AccountTable
-- ----------------------------
ALTER TABLE [dbo].[AccountTable] ADD CONSTRAINT [PK__AccountT__349DA586E13EC640] PRIMARY KEY CLUSTERED ([AccountID])
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
GO
-- ----------------------------
-- Auto increment value for BillingLog
-- ----------------------------
DBCC CHECKIDENT ('[dbo].[BillingLog]', RESEED, 1)
GO
-- ----------------------------
-- Primary Key structure for table BillingLog
-- ----------------------------
ALTER TABLE [dbo].[BillingLog] ADD CONSTRAINT [PK_BillingLog] PRIMARY KEY CLUSTERED ([bid])
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
GO
-- ----------------------------
-- Auto increment value for VerificationCode
-- ----------------------------
DBCC CHECKIDENT ('[dbo].[VerificationCode]', RESEED, 1)
GO
-- ----------------------------
-- Primary Key structure for table VerificationCode
-- ----------------------------
ALTER TABLE [dbo].[VerificationCode] ADD CONSTRAINT [PK__Password__3213E83FA2A48C58] PRIMARY KEY CLUSTERED ([id])
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
GO

View file

@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<service>
<active_area country="usa" />
<area country="usa" auth_url="http://localhost:8070/serverApi/auth" billing_url="http://localhost:8080/serverApi/billing" billing_idc="10101" xtrap_use="0" server_mode="WAG" betazone="0" />
<area country="chn" skip_auth="1" free_cash="1" skip_abuse_nick ="1" enc_xml_use ="1" billing_idc="10101" xtrap_use="0" server_mode="WAG" betazone="0" />
</service>

101
src/app.js Normal file
View file

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

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

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

View file

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

View file

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

View file

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

View file

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

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

@ -0,0 +1,77 @@
const express = require('express');
const bodyParser = require('body-parser');
const bcrypt = require('bcrypt');
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
const sql = require('mssql');
const router = express.Router();
const { authLogger } = require('../utils/logger');
// Set up database connection
const { connAccount } = require('../utils/dbConfig');
// Route for handling login requests
router.post('/', bodyParser.text({
type: '*/xml'
}), async (req, res) => {
try {
const xml = req.body;
const result = await parser.parseStringPromise(xml);
const loginRequest = result['login-request'];
const account = loginRequest.account[0];
const password = loginRequest.password[0];
const game = loginRequest.game[0];
const ip = loginRequest.ip[0];
authLogger.info(`[Auth] Account [${account}] is trying to login from [${ip}]`);
// Create a connection pool for the database
const pool = await connAccount;
// Get the account information from the database
const {
recordset
} = await pool
.request()
.input('Identifier', sql.VarChar(50), account)
.execute('GetAccount');
// Check if the account exists
const row = recordset[0];
if (!row || row.Result !== 'AccountExists') {
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
return res.send('<status>failed</status>');
}
// Verify the password using bcrypt
const passwordMatch = await bcrypt.compare(password, row.AccountPwd);
// Authenticate the user and update the database
const {
recordset: authRecordset
} = await pool
.request()
.input('Identifier', sql.VarChar(50), account)
.input('password_verify_result', sql.Bit, passwordMatch ? 1 : 0)
.input('LastLoginIP', sql.VarChar(50), ip)
.execute('AuthenticateUser');
const authRow = authRecordset[0];
if (!authRow || authRow.Result !== 'LoginSuccess') {
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
return res.send('<status>failed</status>');
}
// Send the authentication response
const response = `<userid>${authRow.AuthID}</userid><user-type>F</user-type><status>success</status>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
res.send(response);
authLogger.info(`[Auth] Account [${account}] successfully logged in from [${ip}]`);
} catch (error) {
authLogger.error('Error handling login request: ' + error.message);
res.status(500).send('<status>failed</status>');
}
});
module.exports = router;

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

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

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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,100 @@
const sql = require('mssql');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const express = require('express');
const router = express.Router();
const { logger, accountLogger } = require('../../utils/logger');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Define the validation schema for the request body
const schema = Joi.object({
account: Joi.string().required(),
password: Joi.string().required(),
});
router.post('/', async (req, res) => {
try {
// Validate the request body against the schema
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const account = value.account;
const password = value.password;
const userIp = req.ip;
// Check the format of the account identifier
if (
!/^[A-Za-z0-9_-]{6,50}$/.test(account) &&
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account)
) {
return res.status(400).json({ Result: 'InvalidUsernameFormat' });
}
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, account);
const result = await request.execute('GetAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountExists') {
const windyCode = row.WindyCode;
const hash = row.AccountPwd;
// Verify the password
const md5_password = crypto
.createHash('md5')
.update(windyCode + password)
.digest('hex');
const password_verify_result = await bcrypt.compare(
md5_password,
hash
);
const authRequest = pool.request();
authRequest.input('Identifier', sql.VarChar, account);
authRequest.input(
'password_verify_result',
sql.Bit,
password_verify_result
);
authRequest.input('LastLoginIP', sql.VarChar, userIp);
const authResult = await authRequest.execute('AuthenticateUser');
const authRow = authResult.recordset[0];
if (authRow && authRow.Result === 'LoginSuccess') {
accountLogger.info(
`[Account] Launcher Login: Account [${windyCode}] successfully logged in from [${userIp}]`
);
return res.status(200).json({
Result: authRow.Result,
Token: authRow.Token,
WindyCode: authRow.WindyCode,
});
} else {
accountLogger.info(
`[Account] Launcher Login: Account [${windyCode}] login failed: ${authRow.Result} `
);
return res.status(400).json({
Result: authRow.Result,
});
}
} else {
return res.status(400).json({ Result: 'AccountNotFound' });
}
} catch (error) {
logger.error(
'[Account] Launcher Login: Database query failed: ' + error.message
);
return res
.status(500)
.send('Database query failed: ' + error.message);
}
});
module.exports = router;

View file

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

View file

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

View file

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

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

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

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

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

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

@ -0,0 +1,144 @@
const fs = require("fs");
const path = require("path");
const util = require("util");
const winston = require("winston");
const logsDirectory = 'logs';
if (!fs.existsSync(logsDirectory)) {
fs.mkdirSync(logsDirectory);
}
const authLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
),
transports: [
new winston.transports.File({
filename: `logs/auth-${new Date().toISOString().slice(0, 10)}.log`,
level: 'info',
filter: (log) => log.message.includes('[Auth]')
})
]
});
if (process.env.LOG_AUTH_CONSOLE === 'true') {
authLogger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
)
}));
}
const billingLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
),
transports: [
new winston.transports.File({
filename: `logs/billing-${new Date().toISOString().slice(0, 10)}.log`,
level: 'info',
filter: (log) => log.message.includes('[Billing]')
})
]
});
if (process.env.LOG_BILLING_CONSOLE === 'true') {
billingLogger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
)
}));
}
const mailerLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
),
transports: [
new winston.transports.File({
filename: `logs/mailer-${new Date().toISOString().slice(0, 10)}.log`,
level: 'info',
filter: (log) => log.message.includes('[Mailer]')
})
]
});
if (process.env.LOG_MAILER_CONSOLE === 'true') {
mailerLogger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
)
}));
}
const accountLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
),
transports: [
new winston.transports.File({
filename: `logs/account-${new Date().toISOString().slice(0, 10)}.log`,
level: 'info',
filter: (log) => log.message.includes('[Account]')
})
]
});
if (process.env.LOG_ACCOUNT_CONSOLE === 'true') {
accountLogger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
)
}));
}
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
)
}),
new winston.transports.File({
filename: `logs/api-${new Date().toISOString().slice(0, 10)}.log`,
level: 'info'
}),
new winston.transports.File({
filename: `logs/error-${new Date().toISOString().slice(0, 10)}.log`,
level: 'error'
})
]
});
module.exports = {
authLogger,
billingLogger,
mailerLogger,
accountLogger,
logger
};