Version 1.3.0
- Implemented authentication and billing routes for Jpn region. - Refactored and changed the project structure from CommonJS to ES Modules
92
.env
|
|
@ -2,34 +2,65 @@
|
||||||
# API CONFIGURATION #
|
# API CONFIGURATION #
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
# Set the port for receiving connections
|
# Set the host for receiving connections from the users for access launcher functions.
|
||||||
PUBLIC_IP=
|
# Use 0.0.0.0 or leave empty for bind API on all IPs.
|
||||||
PORT=3000
|
API_LISTEN_HOST=
|
||||||
AUTH_PORT=8070
|
|
||||||
BILLING_PORT=8080
|
# Set the port for receiving connections from the users for access launcher functions.
|
||||||
|
API_LISTEN_PORT=80
|
||||||
|
|
||||||
|
# Set the host for receiving connections from the GameGatewayServer/ManagerServer servers (Rusty Hearts Servers) for the auth/billing functions.
|
||||||
|
# Use 0.0.0.0 for bind API on all IPs (not recommended!).
|
||||||
|
API_LOCAL_LISTEN_HOST=127.0.0.1
|
||||||
|
|
||||||
|
# Allow determination of client IP address based on "X-Forwarded-For" header.
|
||||||
|
# This must be enabled if a reverse proxy is used. It is also necessary to specify the reverse
|
||||||
|
# proxy IP address in parameter API_TRUSTPROXY_HOSTS, otherwise data spoofing is possible.
|
||||||
|
API_TRUSTPROXY_ENABLE=false
|
||||||
|
|
||||||
|
# List of IP addresses or subnets that should be trusted as a reverse proxy.
|
||||||
|
# Multiple entries can be listed separated by commas.
|
||||||
|
# If left empty, headers will be accepted from any IP address (not recommended!).
|
||||||
|
API_TRUSTPROXY_HOSTS=
|
||||||
|
|
||||||
|
# Set the initial balance value of In-game Shop account on user registration.
|
||||||
|
API_SHOP_INITIAL_BALANCE=0
|
||||||
|
|
||||||
|
# Set the port for receiving connections for the Auth/Billing API (USA).
|
||||||
|
API_USA_PORT=8070
|
||||||
|
|
||||||
|
# Set the port for receiving connections for the Auth/Billing API (JPN).
|
||||||
|
API_JPN_PORT=8080
|
||||||
|
|
||||||
|
# Set the port for receiving connections from the proxy server (JPN).
|
||||||
|
API_PROXY_PORT=8090
|
||||||
|
|
||||||
# Determines whether the helmet middleware is enabled or disabled. If enabled https need to be used for the api.
|
# 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 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.
|
# 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
|
API_ENABLE_HELMET=false
|
||||||
|
|
||||||
# Set the server timezone
|
# Set the server timezone
|
||||||
TZ=America/New_York
|
TZ=UTC
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# LOGGING CONFIGURATION #
|
# LOGGING CONFIGURATION #
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
LOG_LEVEL=info
|
# Set log level (available levels: debug, info, warn, error).
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Enable log IP addresses.
|
||||||
|
LOG_IP_ADDRESSES=false
|
||||||
|
|
||||||
LOG_AUTH_CONSOLE=true
|
LOG_AUTH_CONSOLE=true
|
||||||
LOG_BILLING_CONSOLE=true
|
LOG_BILLING_CONSOLE=true
|
||||||
LOG_ACCOUNT_CONSOLE=false
|
LOG_ACCOUNT_CONSOLE=true
|
||||||
LOG_MAILER_CONSOLE=false
|
LOG_MAILER_CONSOLE=true
|
||||||
|
|
||||||
##################################
|
###########################################
|
||||||
# API DATABASE CONFIGURATION #
|
# API DATABASE CONFIGURATION (SQL Server) #
|
||||||
##################################
|
###########################################
|
||||||
|
|
||||||
# Set a host to connect to the SQL server database.
|
# Set a host to connect to the SQL server database.
|
||||||
DB_SERVER=127.0.0.1
|
DB_SERVER=127.0.0.1
|
||||||
|
|
@ -41,26 +72,30 @@ DB_DATABASE=RustyHearts_Account
|
||||||
DB_USER=sa
|
DB_USER=sa
|
||||||
|
|
||||||
# Set the password to connect to database
|
# Set the password to connect to database
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=@RustyHearts
|
||||||
|
|
||||||
# Set to encrypt the connection to the database
|
# Set to encrypt the connection to the database
|
||||||
DB_ENCRYPT=false
|
DB_ENCRYPT=false
|
||||||
|
|
||||||
##################################
|
#########################
|
||||||
# GATEWAY API CONFIGURATION #
|
# GATEWAY CONFIGURATION #
|
||||||
##################################
|
#########################
|
||||||
|
|
||||||
# Set the host for receiving connections to the gateserver
|
# Set the host for receiving connections to the GameGatewayServer
|
||||||
GATESERVER_IP=YOUR_SERVER_IP
|
GATESERVER_IP=192.168.100.3
|
||||||
|
|
||||||
# Set the port for receiving connections to the gateserver
|
# Set the port for receiving connections to the GameGatewayServer
|
||||||
GATESERVER_PORT=50001
|
GATESERVER_PORT=50001
|
||||||
|
|
||||||
|
# Set the server/world id used in the database
|
||||||
|
SERVER_ID=10101
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# EMAIL CONFIGURATION #
|
# SMTP CONFIGURATION #
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
# using gmail smtp server
|
# using gmail smtp server
|
||||||
# To generate app passwords, first you have to enable 2-Step Verification on our Google account.
|
# To generate app passwords, first you have to enable 2-Step Verification on your Google account.
|
||||||
# Go to your Google account security settings (https://myaccount.google.com/security) and enable 2-Step Verification
|
# 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
|
# Now, you can select the App passwords option to set up a new app password. https://myaccount.google.com/u/2/apppasswords
|
||||||
|
|
||||||
|
|
@ -73,11 +108,14 @@ SMTP_PORT=465
|
||||||
# The encryption protocol to use (e.g. ssl, tls)
|
# The encryption protocol to use (e.g. ssl, tls)
|
||||||
SMTP_ENCRYPTION=ssl
|
SMTP_ENCRYPTION=ssl
|
||||||
|
|
||||||
# your email
|
# The username of the SMTP server
|
||||||
SMTP_USERNAME=your.email@gmail.com
|
SMTP_USERNAME=noreply@example.com
|
||||||
|
|
||||||
# app password
|
# The password/app password of the SMTP server
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|
||||||
# The name to use as the sender in emails
|
# Outgoing mail sender email address.
|
||||||
SMTP_FROMNAME=Rusty Hearts
|
SMTP_EMAIL_FROM_ADDRESS=noreply@example.com
|
||||||
|
|
||||||
|
# Outgoing mail sender name.
|
||||||
|
SMTP_FROM_NAME=Rusty Hearts
|
||||||
|
|
|
||||||
46
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: Create Release and Upload Assets
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Get version from package.json
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
echo "::set-output name=version::$VERSION"
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: "v${{ steps.get_version.outputs.version }}"
|
||||||
|
release_name: "RustyHearts-API v${{ steps.get_version.outputs.version }}"
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Release Assets
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist
|
||||||
|
asset_name: RustyHearts-API-v${{ steps.get_version.outputs.version }}.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
1
.gitignore
vendored
|
|
@ -128,3 +128,4 @@ dist
|
||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
package-lock.json
|
||||||
|
|
|
||||||
103
README.md
|
|
@ -3,25 +3,17 @@
|
||||||
|
|
||||||
RustyHearts-API is a Node.js-based REST API that enables authentication, billing, and launcher functionalities for Rusty Hearts.
|
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.
|
The API consists of independent servers (Auth/Billing API and Launcher API) running on different ports.
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
Either use `git clone https://github.com/JuniorDark/RustyHearts-API` on the command line to clone the repository or use Code --> Download zip button to get the files.
|
|
||||||
|
|
||||||
### Preview
|
|
||||||

|
|
||||||
|
|
||||||
### API region support
|
|
||||||
The api currently only support the **usa** (PWE) region.
|
|
||||||
|
|
||||||
|
### API game region support
|
||||||
* **usa** (PWE) - Full api support
|
* **usa** (PWE) - Full api support
|
||||||
* **chn** (Xunlei) - Only launcher support
|
* **jpn** (SEGA) - Full api support
|
||||||
|
|
||||||
## Server Descriptions
|
### Servers
|
||||||
|
|
||||||
- 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.
|
- **Launcher API**: The Launcher API is as 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 account 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.
|
||||||
- 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.
|
- **Auth/Billing API (USA)/(JPN)**: This API is responsible for in-game authentication and handle the shop balance and purchases. It is recommended to bind this API only to a local IP address and prevent external access to these APIs.
|
||||||
|
- **Proxy (JPN)**: This server is used as a proxy to receive the request with malformed headers send from the game server, and forward it fixed to the Auth/Billing API.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
* [Preview](#preview)
|
* [Preview](#preview)
|
||||||
|
|
@ -33,6 +25,9 @@ The api currently only support the **usa** (PWE) region.
|
||||||
* [Available endpoints](#available-endpoints)
|
* [Available endpoints](#available-endpoints)
|
||||||
* [License](#license)
|
* [License](#license)
|
||||||
|
|
||||||
|
### Preview
|
||||||
|

|
||||||
|
|
||||||
## Public folder description
|
## Public folder description
|
||||||
|
|
||||||
### Launcher self-update
|
### Launcher self-update
|
||||||
|
|
@ -71,22 +66,29 @@ To deploy RustyHearts-API, follow these steps:
|
||||||
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.
|
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.
|
4. Import the [database file](share/RustyHearts_Account.sql) to your Microsoft SQL Server.
|
||||||
5. Configure the parameters in the [**.env**](.env) file.
|
5. Configure the parameters in the [**.env**](.env) file.
|
||||||
6. Start RustyHearts-API servers by executing the `node src/app` command or running the **rh-api.bat** file.
|
6. Start RustyHearts-API servers by running the file **start-JPN** or **start-USA** file.
|
||||||
7. The server region must be set to **usa** on [service_control.xml](share/service_control.xml)
|
7. Set the server region to **usa** or **jpn** on [service_control.xml](share/service_control.xml)
|
||||||
|
|
||||||
## .env file setup:
|
## .env file setup:
|
||||||
|
|
||||||
### API CONFIGURATION
|
### API CONFIGURATION
|
||||||
|
|
||||||
- **PORT**: The port number for receiving connections (default 3000).
|
- **API_LISTEN_HOST**: The host for receiving connections from the users for access public/launcher functions. Use `0.0.0.0` or leave empty to bind API on all IPs.
|
||||||
- **AUTH_PORT**: The port number for the Auth API.
|
- **API_LISTEN_PORT**: The port number for receiving connections from the users for access public/launcher functions (default 80).
|
||||||
- **BILLING_PORT**: The port number for the Billing API.
|
- **API_LOCAL_LISTEN_HOST**: The host for receiving connections from the GameGatewayServer/ManagerServer servers (Rusty Hearts Servers) for the auth/billing functions. Use `127.0.0.1` (recommended) or `0.0.0.0` to bind API on all IPs (not recommended!).
|
||||||
- **ENABLE_HELMET**: Determines whether the helmet middleware is enabled or disabled. If enabled, https need to be used for the api.
|
- **API_USA_PORT**: The port number for receiving connections from the GameGatewayServer/ManagerServer servers (Rusty Hearts Servers) for the auth/billing functions for the usa region.
|
||||||
|
- **API_JPN_PORT**: The port number for receiving connections from the GameGatewayServer/ManagerServer servers (Rusty Hearts Servers) for the auth/billing functions for the jpn region.
|
||||||
|
- **API_PROXY_PORT**: The port number for receiving connections from the GameGatewayServer/ManagerServer servers (Rusty Hearts Servers) for the auth/billing functions for the jpn region. This port is used to receive requests with malformed headers and forward them to the Auth/Billing API.
|
||||||
|
- **API_TRUSTPROXY_ENABLE**: Allow determination of client IP address based on `X-Forwarded-For` header. Must be enabled if a reverse proxy is used.
|
||||||
|
- **API_TRUSTPROXY_HOSTS**: List of IP addresses or subnets that should be trusted as a reverse proxy. Multiple entries can be listed separated by commas. If left empty, headers will be accepted from any IP address (not recommended!).
|
||||||
|
- **API_SHOP_INITIAL_BALANCE**: The initial balance value of the in-game shop on user registration.
|
||||||
|
- **API_ENABLE_HELMET**: Determines whether the helmet middleware is enabled or disabled. If enabled, HTTPS needs to be used for the API.
|
||||||
- **TZ**: The timezone for the server.
|
- **TZ**: The timezone for the server.
|
||||||
|
|
||||||
### LOGGING CONFIGURATION
|
### LOGGING CONFIGURATION
|
||||||
|
|
||||||
- **LOG_LEVEL**: The level of logging to use (e.g. debug, info, warn, error).
|
- **LOG_LEVEL**: The level of logging to use (e.g., debug, info, warn, error).
|
||||||
|
- **LOG_IP_ADDRESSES**: Enable logging of IP addresses.
|
||||||
- **LOG_AUTH_CONSOLE**: Whether to log Auth API messages to the console.
|
- **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_BILLING_CONSOLE**: Whether to log Billing API messages to the console.
|
||||||
- **LOG_ACCOUNT_CONSOLE**: Whether to log Account API messages to the console.
|
- **LOG_ACCOUNT_CONSOLE**: Whether to log Account API messages to the console.
|
||||||
|
|
@ -100,40 +102,57 @@ To deploy RustyHearts-API, follow these steps:
|
||||||
- **DB_PASSWORD**: The password for the database user.
|
- **DB_PASSWORD**: The password for the database user.
|
||||||
- **DB_ENCRYPT**: Whether to encrypt the connection to the database.
|
- **DB_ENCRYPT**: Whether to encrypt the connection to the database.
|
||||||
|
|
||||||
### GATEWAY API CONFIGURATION
|
### GATEWAY CONFIGURATION
|
||||||
|
|
||||||
- **GATESERVER_IP**: The IP address of the gate server.
|
- **GATESERVER_IP**: The IP address of the gate server.
|
||||||
- **GATESERVER_PORT**: The port number of the gate server.
|
- **GATESERVER_PORT**: The port number of the gate server.
|
||||||
|
- **SERVER_ID**: The server/world ID used in the database.
|
||||||
|
|
||||||
### EMAIL CONFIGURATION
|
### EMAIL CONFIGURATION
|
||||||
|
|
||||||
- **SMTP_HOST**: The hostname or IP address of the SMTP server.
|
- **SMTP_HOST**: The hostname or IP address of the SMTP server.
|
||||||
- **SMTP_PORT**: The port number 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_ENCRYPTION**: The encryption protocol to use (e.g., ssl, tls).
|
||||||
- **SMTP_USERNAME**: The username for the SMTP server.
|
- **SMTP_USERNAME**: The username for the SMTP server.
|
||||||
- **SMTP_PASSWORD**: The password for the SMTP server.
|
- **SMTP_PASSWORD**: The password for the SMTP server.
|
||||||
- **SMTP_FROMNAME**: The name to use as the sender in emails.
|
- **SMTP_EMAIL_FROM_ADDRESS**: The outgoing mail sender email address.
|
||||||
|
- **SMTP_FROM_NAME**: The outgoing mail sender name.
|
||||||
|
|
||||||
## Available endpoints
|
## Available endpoints
|
||||||
|
|
||||||
The api provides the following endpoints:
|
The API provides the following endpoints:
|
||||||
|
|
||||||
Endpoint | Method | Arguments | Description
|
### Launcher API
|
||||||
--- | --- | --- | ---
|
|
||||||
/serverApi/auth | POST | XML with account, password, game and IP | Authenticates a user game login based on their account information and sends an XML response with their user ID, user type, and success status. If authentication fails, it sends an XML response with a failure status.
|
Endpoint | Method | Arguments | Content Type | Description
|
||||||
/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.
|
/Register | - | -| - | A basic web page for account registration and password change. |
|
||||||
/serverApi/gateway/info | GET | | Returns an response containing the gateway endpoint. Used by the **chn** region.
|
/launcher/GetGatewayAction | GET | - | XML | Returns the gateway server's IP and port in XML format used by the client to connect to the 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.
|
/launcher/SignupAction | POST | `userName`, `email`, `password`, `verificationCode`| Form URL Encoded | Registers a new user account.
|
||||||
/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.
|
/launcher/LoginAction | POST | `account`, `password` | Form URL Encoded | Authenticates a user by username/email and returns a token if successful. |
|
||||||
/accountApi/login | POST | account, password | Authenticates a user account in the launcher by username or email address and password. Return a token if the authentication is successful (token is currently unsued).
|
/launcher/ResetPasswordAction | POST | `email`, `password`, `verificationCode` | Form URL Encoded | Resets a user's password using a verification code. |
|
||||||
/accountApi/codeVerification | POST | email, verification_code_type, verification_code | Verify a user's email by checking the verification code
|
/launcher/SendPasswordResetEmailAction | POST | `email` | Form URL Encoded | Sends a email with a verification code for password reset to the specified address. |
|
||||||
/accountApi/sendPasswordResetEmail | POST | email | Sends an email with a password reset verification code to the specified email address
|
/launcher/SendVerificationEmailAction | POST | `email` | Form URL Encoded | Sends a email with a verification code for account creation reset to the specified address. |
|
||||||
/accountApi/changePassword | POST | email, password, verification_code | Change the password of a user's account, given the email and password verification code
|
/launcher/VerifyCodeAction | POST | `email`, `verificationCodeType`, `verificationCode` | Form URL Encoded | Validates a verification code. `verificationCodeType`: `Account`, `Password` |
|
||||||
/accountApi/sendVerificationEmail | POST | email | Sends a verification email to the specified email address.
|
/launcher/LauncherAction/getLauncherVersion | GET | - | JSON | Returns the version of the launcher specified in the launcher_info.ini file. |
|
||||||
/launcherApi/launcherUpdater/getLauncherVersion | GET | | Returns the version of the launcher by reading the launcher_info.ini file.
|
/launcher/LauncherAction/updateLauncherVersion | POST | `version` | Form URL Encoded | Download the specified launcher version from the launcher_update folder. |
|
||||||
/launcherApi/launcherUpdater/updateLauncherVersion | POST | version | Download the specified launcher versionr from the launcher_update folder.
|
launcher/GetOnlineCountAction | GET | - | JSON | Returns the number of current online players. |
|
||||||
/serverApi/onlineCount | GET | | Returns the number of online players. Returns a JSON object with the count.
|
|
||||||
|
### Auth/Billing API (USA)
|
||||||
|
|
||||||
|
Endpoint | Method | Arguments | Content Type | Description
|
||||||
|
--- | --- | --- | --- | ---
|
||||||
|
/Auth | POST | `<login-request><account>account</account><password>account + password in md5</password><game>16</game><ip>ip</ip></login-request>`| XML | Authenticates a user in the client by username and password. The password must be hashed using MD5. The game ID is always 16 for the USA region. |
|
||||||
|
/Billing | POST | `<currency-request><userid>account</userid><game>1000</game><server>serverId</server></currency-request>` | XML | Returns the shop balance of the user account. The game ID is always 1000 for the USA region. |
|
||||||
|
/Billing | POST | `<item-purchase-request><userid>account</userid><charid>character GUID</charid><game>1000</game><server>serverId</server><amount>itemPrice</amount><itemid>itemid</itemid><count>itemCount</count><uniqueid>Transaction GUID</uniqueid></item-purchase-request>` | XML | Purchases an item from the shop. The game ID is always 1000 for the USA region. The server ID is the same as the one used in the Auth API. The item ID is the same as the one used in the shop. The transaction GUID is a unique identifier for the purchase. |
|
||||||
|
|
||||||
|
### Auth/Billing API (JPN)
|
||||||
|
|
||||||
|
Endpoint | Method | Arguments | Content Type | Description
|
||||||
|
--- | --- | --- | --- | ---
|
||||||
|
/Auth/cgi-bin/auth_rest_oem.cgi | POST | `service_id`, `product_name`, `original_id`, `original_password`| Form URL Encoded | authenticates a user in the client by username and password. service_id is always `FFFFFFFFFFFFFFFFFF` for the JPN region. The product_name is always empty. |
|
||||||
|
/Billing/S1/ApiPointTotalGetS.php | POST | `product_name`, `original_id`, `original_password`, `auto_charge_exec` | Form URL Encoded | Returns the shop balance of the user account. The auto_charge_exec is always 0. |
|
||||||
|
/Billing/S1/ApiPointMoveS.php | POST | `product_name`, `original_id`, `original_password`, `auto_charge_exec`, `move_point`, ` move_kind`, `item_code` | Form URL Encoded | Purchases an item from the shop. `move_kind` is always 06. `move_point` is the cost of the item `-200`. The `item_code` format is `rsty0000000000001`, where the first 10 digits are the item shopID and the last 3 digits are always `001`. |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This project is licensed under the terms found in [`LICENSE-0BSD`](LICENSE).
|
This project is licensed under the terms found in [`LICENSE-0BSD`](LICENSE).
|
||||||
BIN
api.png
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 74 KiB |
90
config/rateLimits.default.js
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// CHANGES MADE ARE APPLIED ONLY AFTER THE PROCESS IS RESTARTED.
|
||||||
|
|
||||||
|
// All set points of all users are reset when API processes are restarted.
|
||||||
|
|
||||||
|
// ATTENTION!
|
||||||
|
// The client's IP address is used as an identifier. Therefore, if the API is behind a reverse
|
||||||
|
// proxy (nginx, CloudFlare), make sure that parameter "API_TRUSTPROXY_ENABLE" is
|
||||||
|
// set to "TRUE" so that the client's IP address is determined correctly.
|
||||||
|
|
||||||
|
// Configuration parameters can be found here:
|
||||||
|
// https://github.com/animir/node-rate-limiter-flexible/wiki/Options
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
// Launcher
|
||||||
|
launcher: {
|
||||||
|
// POST /launcher/SignupAction
|
||||||
|
signupAction: {
|
||||||
|
points: 20,
|
||||||
|
duration: 3600, // 1 hour
|
||||||
|
blockDuration: 3600,
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcher/LoginAction
|
||||||
|
loginAction: {
|
||||||
|
points: 60,
|
||||||
|
duration: 300, // 5 minutes
|
||||||
|
blockDuration: 1800, // 30 minutes block
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcher/VerifyCodeAction
|
||||||
|
verifyCodeAction: {
|
||||||
|
points: 30,
|
||||||
|
duration: 300,
|
||||||
|
blockDuration: 1800,
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcher/ResetPasswordAction
|
||||||
|
resetPasswordAction: {
|
||||||
|
points: 30,
|
||||||
|
duration: 300,
|
||||||
|
blockDuration: 3600,
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcher/SendPasswordResetEmailAction
|
||||||
|
sendPasswordResetEmailAction: {
|
||||||
|
points: 30,
|
||||||
|
duration: 300, // 15 minutes
|
||||||
|
blockDuration: 3600, // 1 hour
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcher/SendVerificationEmailAction
|
||||||
|
sendVerificationEmailAction: {
|
||||||
|
points: 30,
|
||||||
|
duration: 300,
|
||||||
|
blockDuration: 3600,
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcher/GetGatewayAction
|
||||||
|
getGatewayAction: {
|
||||||
|
points: 30,
|
||||||
|
duration: 300,
|
||||||
|
blockDuration: 600,
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcher/GetOnlineCountAction
|
||||||
|
getOnlineCountAction: {
|
||||||
|
points: 30,
|
||||||
|
duration: 180,
|
||||||
|
blockDuration: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
launcherAction: {
|
||||||
|
|
||||||
|
// POST /launcherAction/getLauncherVersion
|
||||||
|
getLauncherVersion: {
|
||||||
|
points: 30,
|
||||||
|
duration: 1800, // 30 minutes
|
||||||
|
blockDuration: 3600,
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /launcherAction/updateLauncherVersion
|
||||||
|
updateLauncherVersion: {
|
||||||
|
points: 30,
|
||||||
|
duration: 1800, // 30 minutes
|
||||||
|
blockDuration: 3600,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
40
ecosystem.config.cjs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'rh-api-all',
|
||||||
|
script: 'src/app.js',
|
||||||
|
args: 'mainApp usaApp jpnApp proxyServer',
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '1G',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rh-api-usa',
|
||||||
|
script: 'src/app.js',
|
||||||
|
args: 'mainApp usaApp',
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '1G',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rh-api-jpn',
|
||||||
|
script: 'src/app.js',
|
||||||
|
args: 'mainApp jpnApp proxyServer',
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '1G',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
118
env.example
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
##################################
|
||||||
|
# API CONFIGURATION #
|
||||||
|
##################################
|
||||||
|
|
||||||
|
# Set the host for receiving connections from the users for access launcher functions.
|
||||||
|
# Use 0.0.0.0 or leave empty for bind API on all IPs.
|
||||||
|
API_LISTEN_HOST=
|
||||||
|
|
||||||
|
# Set the port for receiving connections from the users for access launcher functions.
|
||||||
|
API_LISTEN_PORT=80
|
||||||
|
|
||||||
|
# Set the host for receiving connections from the GameGatewayServer/ManagerServer servers (Rusty Hearts Servers) for the auth/billing functions.
|
||||||
|
# Use 0.0.0.0 for bind API on all IPs (not recomended!).
|
||||||
|
API_LOCAL_LISTEN_HOST=127.0.0.1
|
||||||
|
|
||||||
|
# Allow determination of client IP address based on "X-Forwarded-For" header.
|
||||||
|
# This must be enabled if a reverse proxy is used. It is also necessary to specify the reverse
|
||||||
|
# proxy IP address in parameter API_TRUSTPROXY_HOSTS, otherwise data spoofing is possible.
|
||||||
|
API_TRUSTPROXY_ENABLE=false
|
||||||
|
|
||||||
|
# List of IP addresses or subnets that should be trusted as a reverse proxy.
|
||||||
|
# Multiple entries can be listed separated by commas.
|
||||||
|
# If left empty, headers will be accepted from any IP address (not recommended!).
|
||||||
|
API_TRUSTPROXY_HOSTS=
|
||||||
|
|
||||||
|
# Set the initial balance value of In-game Shop account on user registration.
|
||||||
|
API_SHOP_INITIAL_BALANCE=0
|
||||||
|
|
||||||
|
# Set the port for receiving connections for the Auth/Billing API (USA).
|
||||||
|
API_USA_PORT=8070
|
||||||
|
# Set the port for receiving connections for the Auth/Billing API (JPN).
|
||||||
|
API_JPN_PORT=8080
|
||||||
|
# Set the port for receiving connections from the proxy server (JPN).
|
||||||
|
API_PROXY_PORT=8090
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
API_ENABLE_HELMET=false
|
||||||
|
|
||||||
|
# Set the server timezone
|
||||||
|
TZ=UTC
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# LOGGING CONFIGURATION #
|
||||||
|
##################################
|
||||||
|
|
||||||
|
# Set log level (available levels: debug, info, warn, error).
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Enable log IP addresses.
|
||||||
|
LOG_IP_ADDRESSES=false
|
||||||
|
|
||||||
|
LOG_AUTH_CONSOLE=true
|
||||||
|
LOG_BILLING_CONSOLE=true
|
||||||
|
LOG_ACCOUNT_CONSOLE=true
|
||||||
|
LOG_MAILER_CONSOLE=true
|
||||||
|
|
||||||
|
###########################################
|
||||||
|
# API DATABASE CONFIGURATION (SQL Server) #
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
# 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=@RustyHearts
|
||||||
|
|
||||||
|
# Set to encrypt the connection to the database
|
||||||
|
DB_ENCRYPT=false
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# GATEWAY CONFIGURATION #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# Set the host for receiving connections to the GameGatewayServer
|
||||||
|
GATESERVER_IP=192.168.100.3
|
||||||
|
|
||||||
|
# Set the port for receiving connections to the GameGatewayServer
|
||||||
|
GATESERVER_PORT=50001
|
||||||
|
|
||||||
|
# Set the server/world id used in the database
|
||||||
|
SERVER_ID=10101
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# SMTP 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
|
||||||
|
|
||||||
|
# The username of the SMTP server
|
||||||
|
SMTP_USERNAME=noreply@example.com
|
||||||
|
|
||||||
|
# The password/app password of the SMTP server
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
|
||||||
|
# Outgoing mail sender email address.
|
||||||
|
SMTP_EMAIL_FROM_ADDRESS=noreply@example.com
|
||||||
|
|
||||||
|
# Outgoing mail sender name.
|
||||||
|
SMTP_FROM_NAME=Rusty Hearts
|
||||||
31
package.json
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "RustyHearts-API",
|
"name": "rustyhearts-api",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
|
"type": "module",
|
||||||
"description": "Rusty Hearts REST API implementation on node.js",
|
"description": "Rusty Hearts REST API implementation on node.js",
|
||||||
"main": "src/app.js",
|
"main": "src/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -22,24 +23,26 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.8.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.5.0",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.2.1",
|
||||||
"express-winston": "^4.2.0",
|
"express-winston": "^4.2.0",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.8",
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.1.0",
|
||||||
"joi": "^17.9.2",
|
"joi": "^17.13.3",
|
||||||
"logger": "^0.0.1",
|
"logger": "^0.0.1",
|
||||||
"moment-timezone": "^0.5.43",
|
"moment-timezone": "^0.5.48",
|
||||||
"mssql": "^11.0.0",
|
"mssql": "^11.0.1",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.9.1",
|
"nodemailer": "^6.10.1",
|
||||||
"pm2": "^6.0.5",
|
"pm2": "^6.0.5",
|
||||||
"winston": "^3.8.2",
|
"rate-limiter-flexible": "^7.1.0",
|
||||||
"xml2js": "^0.6.0"
|
"winston": "^3.17.0",
|
||||||
|
"xml2js": "^0.6.2",
|
||||||
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.15.0"
|
"node": ">=18.15.0"
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
[LAUNCHER]
|
[LAUNCHER]
|
||||||
version=1.2.0
|
version=1.4.0
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>News Panel</title>
|
<title>News Panel</title>
|
||||||
<link rel="stylesheet" href="/launcher/news/style.css">
|
<link rel="stylesheet" href="/launcher/news/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body oncontextmenu="return false;">
|
<body oncontextmenu="return false;">
|
||||||
<div class="slider-container">
|
<div class="slider-container">
|
||||||
|
|
@ -28,20 +28,20 @@
|
||||||
<div class="tab-link" data-tab="info">Info</div>
|
<div class="tab-link" data-tab="info">Info</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content tab events active">
|
<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/Halloween.html">Halloween</a> - <span class="tab-date">20/10/2025</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/Winter.html">Winter</a> - <span class="tab-date">10/12/2025</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>
|
<a href="https://your-website.com/news/Happy_New_Year.html">Happy New Year</a> - <span class="tab-date">01/01/2025</span><br>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content tab notices">
|
<div class="tab-content tab notices">
|
||||||
<a href="#">Notice 1</a> - <span class="tab-date">01/01/2023</span><br>
|
<a href="#">Notice 1</a> - <span class="tab-date">01/01/2025</span><br>
|
||||||
<a href="#">Notice 2</a> - <span class="tab-date">02/01/2023</span><br>
|
<a href="#">Notice 2</a> - <span class="tab-date">02/01/2025</span><br>
|
||||||
<a href="#">Notice 3</a> - <span class="tab-date">03/01/2023</span><br>
|
<a href="#">Notice 3</a> - <span class="tab-date">03/01/2025</span><br>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content tab info">
|
<div class="tab-content tab info">
|
||||||
<a href="#">Info 1</a> - <span class="tab-date">01/01/2023</span><br>
|
<a href="#">Info 1</a> - <span class="tab-date">01/01/2025</span><br>
|
||||||
<a href="#">Info 2</a> - <span class="tab-date">02/01/2023</span><br>
|
<a href="#">Info 2</a> - <span class="tab-date">02/01/2025</span><br>
|
||||||
<a href="#">Info 3</a> - <span class="tab-date">03/01/2023</span><br>
|
<a href="#">Info 3</a> - <span class="tab-date">03/01/2025</span><br>
|
||||||
</div>
|
</div>
|
||||||
<script src="/launcher/news/script.js"></script>
|
<script src="/launcher/news/js/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
134
public/site/Signup.html
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Rusty Hearts - Account Management</title>
|
||||||
|
<link rel="stylesheet" href="/site/css/style.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=MedievalSharp&family=Open+Sans:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="/site/images/rh_logo.png" alt="Rusty Hearts Logo" class="logo">
|
||||||
|
</div>
|
||||||
|
<div class="game-description">
|
||||||
|
<h3>Join the Battle</h3>
|
||||||
|
<p>Experience a action hack'n'slash multiplayer online game with fast-paced and highly-stylized brawling combat combined with a solo or team-based dungeon exploration experience.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="form-tabs">
|
||||||
|
<button class="tab-button active" data-tab="register">Create Account</button>
|
||||||
|
<button class="tab-button" data-tab="reset">Reset Password</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Register Form -->
|
||||||
|
<div id="register-form" class="form-content active">
|
||||||
|
<form id="signupForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="userName">Username</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
<input type="text" id="userName" name="userName" placeholder="6-16 alphanumeric characters" required>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="userNameError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
<input type="email" id="email" name="email" placeholder="your@email.com" required>
|
||||||
|
<button type="button" id="sendVerificationBtn" class="verification-btn">Send Code</button>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="emailError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="verificationCode">Verification Code</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
<input type="text" id="verificationCode" name="verificationCode" placeholder="Enter verification code" required>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="verificationCodeError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
<input type="password" id="password" name="password" placeholder="8-16 characters" required>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="passwordError"></div>
|
||||||
|
</div>
|
||||||
|
<div id="registerResponse" class="response-message"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset Password Form -->
|
||||||
|
<div id="reset-form" class="form-content">
|
||||||
|
<form id="resetPasswordForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resetEmail">Email</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
<input type="email" id="resetEmail" name="email" placeholder="your@email.com" required>
|
||||||
|
<button type="button" id="sendResetVerificationBtn" class="verification-btn">Send Code</button>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="resetEmailError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resetVerificationCode">Verification Code</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
<input type="text" id="resetVerificationCode" name="verificationCode" placeholder="Enter verification code" required>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="resetVerificationCodeError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newPassword">New Password</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
<input type="password" id="newPassword" name="password" placeholder="8-16 characters" required>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="newPasswordError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmPassword">Confirm Password</label>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
<input type="password" id="confirmPassword" placeholder="Repeat your password" required>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="confirmPasswordError"></div>
|
||||||
|
</div>
|
||||||
|
<div id="resetResponse" class="response-message"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic Footer -->
|
||||||
|
<div class="form-footer">
|
||||||
|
<div class="footer-content register-footer active">
|
||||||
|
<button type="submit" form="signupForm" class="submit-btn">Create Account</button>
|
||||||
|
<div class="footer-links">
|
||||||
|
<div class="reset-link">
|
||||||
|
Forgot password? <a href="#" class="switch-tab" data-tab="reset">Reset Password</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-content reset-footer">
|
||||||
|
<button type="submit" form="resetPasswordForm" class="submit-btn">Reset Password</button>
|
||||||
|
<div class="footer-links">
|
||||||
|
<div class="register-link">
|
||||||
|
Need an account? <a href="#" class="switch-tab" data-tab="register">Create Account</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/site/js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
442
public/site/css/style.css
Normal file
|
|
@ -0,0 +1,442 @@
|
||||||
|
/* Reset and Base Styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('/site/images/rh1920x1200.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
color: #e0e0e0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
min-height: 600px;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Panel Styles */
|
||||||
|
.left-panel {
|
||||||
|
background-color: white;
|
||||||
|
flex: 1;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
padding: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-description {
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-description h2, h3 {
|
||||||
|
font-family: 'MedievalSharp', cursive;
|
||||||
|
color: #c62828;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-description p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Panel Styles */
|
||||||
|
.right-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container h2 {
|
||||||
|
font-family: 'MedievalSharp', cursive;
|
||||||
|
color: #c62828;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#signupForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon i {
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 120px 12px 40px;
|
||||||
|
border: 2px solid #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon input:focus {
|
||||||
|
border-color: #c62828;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 5px rgba(198, 40, 40, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #ff5252;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-scrollable {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better scrollbar styling */
|
||||||
|
.form-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content::-webkit-scrollbar-track {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #c62828;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Footer - Always Visible */
|
||||||
|
.form-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px 0;
|
||||||
|
margin-top: auto;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #c62828;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
background-color: #b71c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link,
|
||||||
|
.reset-link,
|
||||||
|
.register-link {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link a {
|
||||||
|
color: #c62828;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-message {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-message.success {
|
||||||
|
background-color: rgba(76, 175, 80, 0.2);
|
||||||
|
color: #4CAF50;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-message.error {
|
||||||
|
background-color: rgba(244, 67, 54, 0.2);
|
||||||
|
color: #f44336;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-links a {
|
||||||
|
color: #777;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-links a:hover {
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Styles */
|
||||||
|
.footer-content {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-tab {
|
||||||
|
color: #c62828;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-tab:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.footer-links {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 700px) {
|
||||||
|
.container {
|
||||||
|
max-height: 95vh;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon input {
|
||||||
|
padding: 10px 100px 10px 35px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: none;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-screenshot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verification Button Styles */
|
||||||
|
.verification-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #c62828;
|
||||||
|
border: 1px solid #c62828;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-btn:hover {
|
||||||
|
background-color: #c62828;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-btn:disabled {
|
||||||
|
background-color: #333;
|
||||||
|
color: #777;
|
||||||
|
border-color: #444;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Styles */
|
||||||
|
.form-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-family: 'MedievalSharp', cursive;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Content */
|
||||||
|
.form-content {
|
||||||
|
display: none;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit Buttons */
|
||||||
|
#resetSubmit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content.active {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.password-match {
|
||||||
|
border-color: #4CAF50 !important;
|
||||||
|
box-shadow: 0 0 5px rgba(76, 175, 80, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-mismatch {
|
||||||
|
border-color: #f44336 !important;
|
||||||
|
box-shadow: 0 0 5px rgba(244, 67, 54, 0.5) !important;
|
||||||
|
}
|
||||||
BIN
public/site/images/001.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/site/images/002.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
public/site/images/006.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
public/site/images/012.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/site/images/020.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
public/site/images/021.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
public/site/images/022.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
public/site/images/023.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/site/images/rh.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
public/site/images/rh1920x1200.jpg
Normal file
|
After Width: | Height: | Size: 903 KiB |
BIN
public/site/images/rh_logo.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
461
public/site/js/script.js
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
function setRandomBackground() {
|
||||||
|
const images = ["001.jpg", "002.jpg", "006.jpg", "012.jpg", "020.jpg", "021.jpg", "022.jpg", "023.jpg"];
|
||||||
|
const randomImage = images[Math.floor(Math.random() * images.length)];
|
||||||
|
const panel = document.querySelector(".left-panel");
|
||||||
|
if (panel) {
|
||||||
|
panel.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url('/site/images/${randomImage}')`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRandomBackground();
|
||||||
|
// Tab switching functionality
|
||||||
|
const tabs = document.querySelectorAll(".tab-button");
|
||||||
|
const formContents = document.querySelectorAll(".form-content");
|
||||||
|
const footers = document.querySelectorAll(".footer-content");
|
||||||
|
|
||||||
|
// Switch tab function
|
||||||
|
function switchTab(tabName) {
|
||||||
|
// Update tabs
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
tab.classList.toggle("active", tab.getAttribute("data-tab") === tabName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update forms
|
||||||
|
formContents.forEach((form) => {
|
||||||
|
form.classList.toggle("active", form.id === `${tabName}-form`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update footers
|
||||||
|
footers.forEach((footer) => {
|
||||||
|
footer.classList.toggle(
|
||||||
|
"active",
|
||||||
|
footer.classList.contains(`${tabName}-footer`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab click event
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
tab.addEventListener("click", function () {
|
||||||
|
const tabName = this.getAttribute("data-tab");
|
||||||
|
switchTab(tabName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch tab link click event
|
||||||
|
document.querySelectorAll(".switch-tab").forEach((link) => {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const tabName = this.getAttribute("data-tab");
|
||||||
|
switchTab(tabName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register form functionality
|
||||||
|
const signupForm = document.getElementById("signupForm");
|
||||||
|
const registerResponse = document.getElementById("registerResponse");
|
||||||
|
const sendVerificationBtn = document.getElementById("sendVerificationBtn");
|
||||||
|
let cooldownInterval;
|
||||||
|
|
||||||
|
// Password reset form functionality
|
||||||
|
const resetPasswordForm = document.getElementById("resetPasswordForm");
|
||||||
|
const resetResponse = document.getElementById("resetResponse");
|
||||||
|
const sendResetVerificationBtn = document.getElementById(
|
||||||
|
"sendResetVerificationBtn"
|
||||||
|
);
|
||||||
|
let resetCooldownInterval;
|
||||||
|
|
||||||
|
// Shared functions
|
||||||
|
function startCooldown(button, intervalVar, seconds = 60) {
|
||||||
|
let secondsLeft = seconds;
|
||||||
|
updateButtonText(button, secondsLeft);
|
||||||
|
|
||||||
|
intervalVar = setInterval(() => {
|
||||||
|
secondsLeft--;
|
||||||
|
updateButtonText(button, secondsLeft);
|
||||||
|
|
||||||
|
if (secondsLeft <= 0) {
|
||||||
|
clearInterval(intervalVar);
|
||||||
|
resetVerificationButton(button);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return intervalVar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButtonText(button, seconds) {
|
||||||
|
button.textContent = `Resend (${seconds}s)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetVerificationButton(button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = "Send Code";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(elementId, message) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearErrorMessages(formPrefix = "") {
|
||||||
|
const errorElements = document.querySelectorAll(
|
||||||
|
`.error-message${formPrefix ? `[id^=${formPrefix}]` : ""}`
|
||||||
|
);
|
||||||
|
errorElements.forEach((element) => {
|
||||||
|
element.textContent = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResponseMessage(element, message, type) {
|
||||||
|
element.textContent = message;
|
||||||
|
element.className = "response-message " + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verification code sending functionality for REGISTER form
|
||||||
|
sendVerificationBtn.addEventListener("click", async function () {
|
||||||
|
const email = document.getElementById("email").value.trim();
|
||||||
|
|
||||||
|
// Clear previous error
|
||||||
|
document.getElementById("emailError").textContent = "";
|
||||||
|
|
||||||
|
// Basic email validation
|
||||||
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
showError("emailError", "Please enter a valid email address");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable button and start cooldown
|
||||||
|
sendVerificationBtn.disabled = true;
|
||||||
|
cooldownInterval = startCooldown(sendVerificationBtn, cooldownInterval);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/launcher/SendVerificationEmailAction", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.success) {
|
||||||
|
if (data.message === "AccountExists") {
|
||||||
|
showError("emailError", "Email is already registered");
|
||||||
|
} else {
|
||||||
|
showError(
|
||||||
|
"emailError",
|
||||||
|
"Failed to send verification code: " + data.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
resetVerificationButton(sendVerificationBtn);
|
||||||
|
} else {
|
||||||
|
showResponseMessage(
|
||||||
|
registerResponse,
|
||||||
|
"Verification code sent to your email",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending verification:", error);
|
||||||
|
showError("emailError", "Failed to send verification code");
|
||||||
|
resetVerificationButton(sendVerificationBtn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verification code sending functionality for PASSWORD RESET form
|
||||||
|
sendResetVerificationBtn.addEventListener("click", async function () {
|
||||||
|
const email = document.getElementById("resetEmail").value.trim();
|
||||||
|
|
||||||
|
clearErrorMessages("reset");
|
||||||
|
document.getElementById("resetEmailError").textContent = "";
|
||||||
|
|
||||||
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
showError("resetEmailError", "Please enter a valid email address");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResetVerificationBtn.disabled = true;
|
||||||
|
resetCooldownInterval = startCooldown(
|
||||||
|
sendResetVerificationBtn,
|
||||||
|
resetCooldownInterval
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/launcher/SendPasswordResetEmailAction", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.success) {
|
||||||
|
if (data.message === "AccountNotFound") {
|
||||||
|
showError("resetEmailError", "No account found with this email");
|
||||||
|
} else {
|
||||||
|
showError(
|
||||||
|
"resetEmailError",
|
||||||
|
"Failed to send verification code: " + data.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
resetVerificationButton(sendResetVerificationBtn);
|
||||||
|
} else {
|
||||||
|
showResponseMessage(
|
||||||
|
resetResponse,
|
||||||
|
"Password reset code sent to your email",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending verification:", error);
|
||||||
|
showError("resetEmailError", "Failed to send verification code");
|
||||||
|
resetVerificationButton(sendResetVerificationBtn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission handlers
|
||||||
|
signupForm.addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
clearErrorMessages();
|
||||||
|
showResponseMessage(registerResponse, "", "");
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
userName: document.getElementById("userName").value.trim(),
|
||||||
|
email: document.getElementById("email").value.trim(),
|
||||||
|
password: document.getElementById("password").value.trim(),
|
||||||
|
verificationCode: document
|
||||||
|
.getElementById("verificationCode")
|
||||||
|
.value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/launcher/SignupAction", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
if (data.success) {
|
||||||
|
showResponseMessage(
|
||||||
|
registerResponse,
|
||||||
|
"Account created successfully!",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
handleServerErrors(data.message, formData, "");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showResponseMessage(
|
||||||
|
registerResponse,
|
||||||
|
data.message || "An error occurred. Please try again.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
showResponseMessage(
|
||||||
|
registerResponse,
|
||||||
|
"An error occurred. Please try again.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resetPasswordForm.addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
clearErrorMessages("reset");
|
||||||
|
showResponseMessage(resetResponse, "", "");
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
email: document.getElementById("resetEmail").value.trim(),
|
||||||
|
password: document.getElementById("newPassword").value.trim(),
|
||||||
|
verificationCode: document
|
||||||
|
.getElementById("resetVerificationCode")
|
||||||
|
.value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate the form
|
||||||
|
if (!validateResetForm(formData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/launcher/ResetPasswordAction", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
if (data.success) {
|
||||||
|
showResponseMessage(
|
||||||
|
resetResponse,
|
||||||
|
"Password changed successfully!",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
// Clear password fields on success
|
||||||
|
document.getElementById("newPassword").value = "";
|
||||||
|
document.getElementById("confirmPassword").value = "";
|
||||||
|
} else {
|
||||||
|
handleServerErrors(data.message, formData, "reset");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showResponseMessage(
|
||||||
|
resetResponse,
|
||||||
|
data.message || "An error occurred. Please try again.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
showResponseMessage(
|
||||||
|
resetResponse,
|
||||||
|
"An error occurred. Please try again.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateResetForm(formData) {
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (!formData.email) {
|
||||||
|
showError("resetEmailError", "Email is required");
|
||||||
|
isValid = false;
|
||||||
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||||
|
showError("resetEmailError", "Please enter a valid email address");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verification code validation
|
||||||
|
if (!formData.verificationCode) {
|
||||||
|
showError("resetVerificationCodeError", "Verification code is required");
|
||||||
|
isValid = false;
|
||||||
|
} else if (!/^[0-9]+$/.test(formData.verificationCode)) {
|
||||||
|
showError(
|
||||||
|
"resetVerificationCodeError",
|
||||||
|
"Verification code must contain only numbers"
|
||||||
|
);
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password validation
|
||||||
|
const newPassword = document.getElementById("newPassword").value.trim();
|
||||||
|
const confirmPassword = document
|
||||||
|
.getElementById("confirmPassword")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
|
if (!newPassword) {
|
||||||
|
showError("newPasswordError", "Password is required");
|
||||||
|
isValid = false;
|
||||||
|
} else if (newPassword.length < 8 || newPassword.length > 16) {
|
||||||
|
showError("newPasswordError", "Password must be 8-16 characters");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmPassword) {
|
||||||
|
showError("confirmPasswordError", "Please confirm your password");
|
||||||
|
isValid = false;
|
||||||
|
} else if (newPassword !== confirmPassword) {
|
||||||
|
showError("confirmPasswordError", "Passwords do not match");
|
||||||
|
showError("newPasswordError", "Passwords do not match");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// real-time password matching feedback
|
||||||
|
const newPasswordInput = document.getElementById("newPassword");
|
||||||
|
const confirmPasswordInput = document.getElementById("confirmPassword");
|
||||||
|
|
||||||
|
function checkPasswordMatch() {
|
||||||
|
const newPassword = newPasswordInput.value;
|
||||||
|
const confirmPassword = confirmPasswordInput.value;
|
||||||
|
|
||||||
|
if (newPassword && confirmPassword) {
|
||||||
|
if (newPassword === confirmPassword) {
|
||||||
|
newPasswordInput.classList.add("password-match");
|
||||||
|
newPasswordInput.classList.remove("password-mismatch");
|
||||||
|
confirmPasswordInput.classList.add("password-match");
|
||||||
|
confirmPasswordInput.classList.remove("password-mismatch");
|
||||||
|
showError("confirmPasswordError", "");
|
||||||
|
showError("newPasswordError", "");
|
||||||
|
} else {
|
||||||
|
newPasswordInput.classList.add("password-mismatch");
|
||||||
|
newPasswordInput.classList.remove("password-match");
|
||||||
|
confirmPasswordInput.classList.add("password-mismatch");
|
||||||
|
confirmPasswordInput.classList.remove("password-match");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPasswordInput.classList.remove("password-match", "password-mismatch");
|
||||||
|
confirmPasswordInput.classList.remove(
|
||||||
|
"password-match",
|
||||||
|
"password-mismatch"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newPasswordInput.addEventListener("input", checkPasswordMatch);
|
||||||
|
confirmPasswordInput.addEventListener("input", checkPasswordMatch);
|
||||||
|
|
||||||
|
function handleServerErrors(errorCode, formData, prefix = "") {
|
||||||
|
switch (errorCode) {
|
||||||
|
case "UsernameExists":
|
||||||
|
showError(`${prefix}userNameError`, "Username is already in use");
|
||||||
|
break;
|
||||||
|
case "EmailExists":
|
||||||
|
showError(`${prefix}EmailError`, "Email is already registered");
|
||||||
|
break;
|
||||||
|
case "AccountNotFound":
|
||||||
|
showError(`${prefix}EmailError`, "No account found with this email");
|
||||||
|
break;
|
||||||
|
case "InvalidVerificationCode":
|
||||||
|
showError(
|
||||||
|
`${prefix}VerificationCodeError`,
|
||||||
|
"Invalid verification code"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "ExpiredVerificationCode":
|
||||||
|
showError(
|
||||||
|
`${prefix}VerificationCodeError`,
|
||||||
|
"Verification code has expired, please request a new one"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "SamePassword":
|
||||||
|
showResponseMessage(
|
||||||
|
resetResponse,
|
||||||
|
"New password cannot be the same as the old password",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
const responseElement = prefix ? resetResponse : registerResponse;
|
||||||
|
showResponseMessage(
|
||||||
|
responseElement,
|
||||||
|
"An error occurred: " + errorCode,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
@echo off
|
|
||||||
title Rusty Hearts API
|
|
||||||
node src/app
|
|
||||||
pause
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
@echo off
|
|
||||||
title API
|
|
||||||
cmd /k "npx pm2 start src/app.js --name rh-api && npx pm2 logs rh-api"
|
|
||||||
pause
|
|
||||||
145
share/RustyHearts_Account Procedures Update 1.3.sql
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
-- ----------------------------
|
||||||
|
-- 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),
|
||||||
|
@ServerId int,
|
||||||
|
@ShopBalance Bigint
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @Result varchar(20)
|
||||||
|
DECLARE @EmailExists int;
|
||||||
|
DECLARE @UsernameExists int;
|
||||||
|
DECLARE @WindyCodeExists int;
|
||||||
|
|
||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION
|
||||||
|
|
||||||
|
|
||||||
|
SELECT @EmailExists = COUNT(*) FROM AccountTable
|
||||||
|
WHERE Email = @Email;
|
||||||
|
SELECT @UsernameExists = COUNT(*) FROM AccountTable
|
||||||
|
WHERE WindyCode = @WindyCode;
|
||||||
|
SELECT @WindyCodeExists = COUNT(*) FROM RustyHearts_Auth.dbo.AuthTable
|
||||||
|
WHERE WindyCode = @WindyCode;
|
||||||
|
|
||||||
|
|
||||||
|
-- Check if account exists
|
||||||
|
IF @EmailExists > 0
|
||||||
|
SET @Result = 'EmailExists';
|
||||||
|
ELSE IF @UsernameExists > 0
|
||||||
|
SET @Result = 'UsernameExists';
|
||||||
|
ELSE IF @WindyCodeExists > 0
|
||||||
|
SET @Result = 'UsernameExists';
|
||||||
|
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, @ServerId, @ShopBalance);
|
||||||
|
|
||||||
|
|
||||||
|
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 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
|
||||||
|
BEGIN
|
||||||
|
SET @Result = 'AccountExists';
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
SELECT @Result as Result;
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -1,21 +1,3 @@
|
||||||
/*
|
|
||||||
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
|
-- Table structure for AccountTable
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
@ -495,33 +477,40 @@ IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[Cr
|
||||||
GO
|
GO
|
||||||
|
|
||||||
CREATE PROCEDURE [dbo].[CreateAccount]
|
CREATE PROCEDURE [dbo].[CreateAccount]
|
||||||
@WindyCode varchar(50),
|
@WindyCode varchar(50),
|
||||||
@AccountPwd varchar(255),
|
@AccountPwd varchar(255),
|
||||||
@Email varchar(255),
|
@Email varchar(255),
|
||||||
@RegisterIP varchar(16)
|
@RegisterIP varchar(16),
|
||||||
|
@ServerId int,
|
||||||
|
@ShopBalance Bigint
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON;
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
DECLARE @Result varchar(20)
|
DECLARE @Result varchar(20)
|
||||||
DECLARE @AccountExists int;
|
DECLARE @EmailExists int;
|
||||||
|
DECLARE @UsernameExists int;
|
||||||
DECLARE @WindyCodeExists int;
|
DECLARE @WindyCodeExists int;
|
||||||
|
|
||||||
BEGIN TRY
|
BEGIN TRY
|
||||||
BEGIN TRANSACTION
|
BEGIN TRANSACTION
|
||||||
|
|
||||||
|
|
||||||
SELECT @AccountExists = COUNT(*) FROM AccountTable
|
SELECT @EmailExists = COUNT(*) FROM AccountTable
|
||||||
WHERE WindyCode = @WindyCode OR Email = @Email;
|
WHERE Email = @Email;
|
||||||
|
SELECT @UsernameExists = COUNT(*) FROM AccountTable
|
||||||
|
WHERE WindyCode = @WindyCode;
|
||||||
SELECT @WindyCodeExists = COUNT(*) FROM RustyHearts_Auth.dbo.AuthTable
|
SELECT @WindyCodeExists = COUNT(*) FROM RustyHearts_Auth.dbo.AuthTable
|
||||||
WHERE WindyCode = @WindyCode;
|
WHERE WindyCode = @WindyCode;
|
||||||
|
|
||||||
|
|
||||||
-- Check if account exists
|
-- Check if account exists
|
||||||
IF @AccountExists > 0
|
IF @EmailExists > 0
|
||||||
SET @Result = 'AccountExists';
|
SET @Result = 'EmailExists';
|
||||||
|
ELSE IF @UsernameExists > 0
|
||||||
|
SET @Result = 'UsernameExists';
|
||||||
ELSE IF @WindyCodeExists > 0
|
ELSE IF @WindyCodeExists > 0
|
||||||
SET @Result = 'WindyCodeExists';
|
SET @Result = 'UsernameExists';
|
||||||
ELSE
|
ELSE
|
||||||
SET @Result = 'NewUser';
|
SET @Result = 'NewUser';
|
||||||
|
|
||||||
|
|
@ -535,7 +524,7 @@ BEGIN
|
||||||
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', '');
|
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)
|
INSERT INTO CashTable (WindyCode, WorldId, Zen)
|
||||||
VALUES (@WindyCode, 10101, 0);
|
VALUES (@WindyCode, @ServerId, @ShopBalance);
|
||||||
|
|
||||||
|
|
||||||
SET @Result = 'AccountCreated';
|
SET @Result = 'AccountCreated';
|
||||||
|
|
@ -664,10 +653,13 @@ BEGIN
|
||||||
|
|
||||||
|
|
||||||
-- Check if account exists
|
-- Check if account exists
|
||||||
IF @AccountExists > 0
|
IF @AccountExists > 0
|
||||||
|
BEGIN
|
||||||
SET @Result = 'AccountExists';
|
SET @Result = 'AccountExists';
|
||||||
ELSE
|
COMMIT TRANSACTION;
|
||||||
SET @Result = 'AccountDontExists';
|
SELECT @Result as Result;
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
IF @Result = 'AccountDontExists'
|
IF @Result = 'AccountDontExists'
|
||||||
-- Retrieve count of existing verification codes for the user
|
-- Retrieve count of existing verification codes for the user
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" ?>
|
<?xml version="1.0" ?>
|
||||||
|
|
||||||
<service>
|
<service>
|
||||||
<active_area country="usa" />
|
<active_area country="jpn" />
|
||||||
|
<area country="usa" auth_url="http://127.0.0.1:8070/Auth" billing_url="http://127.0.0.1:8070/Billing" billing_idc="10101" xtrap_use="0" server_mode="WAG" betazone="0" />
|
||||||
<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="jpn" auth_url="http://127.0.0.1:8090/Auth" billing_url="http://127.0.0.1:8090/Billing/" skip_auth="0" skip_billing="0" free_cash="0" skip_abuse_nick="0" second_pass="0" betazone="0" server_mode="WAG" channel_limit_count="1" world_user_limit_count="100" billing_idc="10101" />
|
||||||
|
<area country="chn" skip_auth="1" free_cash="1" skip_abuse_nick ="1" 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>
|
</service>
|
||||||
|
|
|
||||||
185
src/app.js
|
|
@ -1,163 +1,48 @@
|
||||||
// Load environment variables
|
import config from './config.js';
|
||||||
const env = require('./utils/env');
|
const { logger } = config;
|
||||||
|
import { logSystemInfo } from './utils/systemInfo.js';
|
||||||
|
import memoryLogger from './utils/memoryLogger.js';
|
||||||
|
const { setupMemoryLogging } = memoryLogger;
|
||||||
|
|
||||||
// Import modules
|
// Import servers
|
||||||
const express = require('express');
|
import { startServer as startMainApp } from './servers/mainApp.js';
|
||||||
const helmet = require('helmet');
|
import { startServer as startUsaApp } from './servers/usaApp.js';
|
||||||
const cors = require('cors');
|
import { startServer as startJpnApp } from './servers/jpnApp.js';
|
||||||
const compression = require('compression');
|
import { startServer as startProxyServer } from './servers/proxyServer.js';
|
||||||
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 routes
|
// Parse command line arguments
|
||||||
const authRouter = require('./routes/auth');
|
const args = process.argv.slice(2);
|
||||||
const billingRouter = require('./routes/billing');
|
const serversToStart = args.length > 0 ? args : ['mainApp'];
|
||||||
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');
|
|
||||||
|
|
||||||
// Set up rate limiter
|
// Start selected servers
|
||||||
const limiter = rateLimit({
|
const activeServers = [];
|
||||||
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();
|
if (serversToStart.includes('mainApp')) {
|
||||||
|
activeServers.push(startMainApp());
|
||||||
// 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);
|
if (serversToStart.includes('usaApp')) {
|
||||||
|
activeServers.push(startUsaApp());
|
||||||
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]}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start server
|
if (serversToStart.includes('jpnApp')) {
|
||||||
const port = process.env.PORT || 3000;
|
activeServers.push(startJpnApp());
|
||||||
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('--------------------------------------------------');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log memory usage every 30 minutes (1800000 milliseconds)
|
if (serversToStart.includes('proxyServer')) {
|
||||||
const memoryLogInterval = 1800000;
|
activeServers.push(startProxyServer());
|
||||||
|
}
|
||||||
|
|
||||||
setInterval(logMemoryUsage, memoryLogInterval);
|
// System Info
|
||||||
|
logSystemInfo();
|
||||||
|
|
||||||
app.listen(port, publicIP, () => {
|
// Memory Logging
|
||||||
logger.info(`API listening on ${publicIP}:${port}`);
|
const stopMemoryLogging = setupMemoryLogging();
|
||||||
logger.info(`Auth API listening on 127.0.0.1:${authPort}`);
|
|
||||||
logger.info(`Billing API listening on 127.0.0.1:${billingPort}`);
|
// Handle shutdown
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
logger.info('Shutting down servers...');
|
||||||
|
activeServers.forEach(server => server.close());
|
||||||
|
stopMemoryLogging();
|
||||||
|
process.exit();
|
||||||
});
|
});
|
||||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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');
|
import { createTransport } from 'nodemailer';
|
||||||
const handlebars = require('handlebars');
|
import handlebars from 'handlebars';
|
||||||
const fs = require('fs');
|
import { readFileSync } from 'fs';
|
||||||
const path = require('path');
|
import { logger, mailerLogger } from '../utils/logger.js';
|
||||||
const { mailerLogger } = require('../utils/logger');
|
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 = {
|
const emailTemplates = {
|
||||||
confirmation: fs.readFileSync(path.join(__dirname, 'templates', 'confirmationTemplate.hbs'), 'utf-8'),
|
confirmation: readFileSync(path.join(__dirname, 'templates', 'confirmationTemplate.hbs'), 'utf-8'),
|
||||||
verification: fs.readFileSync(path.join(__dirname, 'templates', 'verificationTemplate.hbs'), 'utf-8'),
|
verification: readFileSync(path.join(__dirname, 'templates', 'verificationTemplate.hbs'), 'utf-8'),
|
||||||
passwordReset: fs.readFileSync(path.join(__dirname, 'templates', 'passwordResetTemplate.hbs'), 'utf-8'),
|
passwordReset: readFileSync(path.join(__dirname, 'templates', 'passwordResetTemplate.hbs'), 'utf-8'),
|
||||||
passwordChanged: fs.readFileSync(path.join(__dirname, 'templates', 'passwordChangedTemplate.hbs'), 'utf-8')
|
passwordChanged: readFileSync(path.join(__dirname, 'templates', 'passwordChangedTemplate.hbs'), 'utf-8')
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compile the email templates
|
|
||||||
const compiledTemplates = {
|
const compiledTemplates = {
|
||||||
confirmation: handlebars.compile(emailTemplates.confirmation),
|
confirmation: handlebars.compile(emailTemplates.confirmation),
|
||||||
verification: handlebars.compile(emailTemplates.verification),
|
verification: handlebars.compile(emailTemplates.verification),
|
||||||
|
|
@ -20,23 +26,47 @@ const compiledTemplates = {
|
||||||
passwordChanged: handlebars.compile(emailTemplates.passwordChanged)
|
passwordChanged: handlebars.compile(emailTemplates.passwordChanged)
|
||||||
};
|
};
|
||||||
|
|
||||||
// SMTP transport configuration
|
// Check for required environment variables
|
||||||
const transporter = nodemailer.createTransport({
|
function isMailerConfigured() {
|
||||||
host: process.env.SMTP_HOST,
|
const requiredEnvVars = [
|
||||||
port: process.env.SMTP_PORT,
|
'SMTP_HOST',
|
||||||
secure: process.env.SMTP_ENCRYPTION === 'ssl' || process.env.SMTP_ENCRYPTION === 'tls',
|
'SMTP_PORT',
|
||||||
auth: {
|
'SMTP_ENCRYPTION',
|
||||||
user: process.env.SMTP_USERNAME,
|
'SMTP_USERNAME',
|
||||||
pass: process.env.SMTP_PASSWORD
|
'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) {
|
function sendConfirmationEmail(email, windyCode) {
|
||||||
const template = compiledTemplates.confirmation;
|
if (!isMailerConfigured()) return;
|
||||||
const emailContent = template({ windyCode });
|
|
||||||
|
const transporter = getTransporter();
|
||||||
|
const emailContent = compiledTemplates.confirmation({ windyCode });
|
||||||
|
|
||||||
const mailOptions = {
|
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,
|
to: email,
|
||||||
subject: '[Rusty Hearts] Account Creation Confirmation',
|
subject: '[Rusty Hearts] Account Creation Confirmation',
|
||||||
html: emailContent
|
html: emailContent
|
||||||
|
|
@ -52,11 +82,13 @@ function sendConfirmationEmail(email, windyCode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendVerificationEmail(email, verificationCode) {
|
function sendVerificationEmail(email, verificationCode) {
|
||||||
const template = compiledTemplates.verification;
|
if (!isMailerConfigured()) return;
|
||||||
const emailContent = template({ verificationCode });
|
|
||||||
|
const transporter = getTransporter();
|
||||||
|
const emailContent = compiledTemplates.verification({ verificationCode });
|
||||||
|
|
||||||
const mailOptions = {
|
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,
|
to: email,
|
||||||
subject: '[Rusty Hearts] Account Creation',
|
subject: '[Rusty Hearts] Account Creation',
|
||||||
html: emailContent
|
html: emailContent
|
||||||
|
|
@ -72,11 +104,13 @@ function sendVerificationEmail(email, verificationCode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendPasswordResetEmail(email, verificationCode) {
|
function sendPasswordResetEmail(email, verificationCode) {
|
||||||
const template = compiledTemplates.passwordReset;
|
if (!isMailerConfigured()) return;
|
||||||
const emailContent = template({ verificationCode });
|
|
||||||
|
const transporter = getTransporter();
|
||||||
|
const emailContent = compiledTemplates.passwordReset({ verificationCode });
|
||||||
|
|
||||||
const mailOptions = {
|
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,
|
to: email,
|
||||||
subject: '[Rusty Hearts] Password Reset Request',
|
subject: '[Rusty Hearts] Password Reset Request',
|
||||||
html: emailContent
|
html: emailContent
|
||||||
|
|
@ -92,11 +126,13 @@ function sendPasswordResetEmail(email, verificationCode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendPasswordChangedEmail(email, windyCode) {
|
function sendPasswordChangedEmail(email, windyCode) {
|
||||||
const template = compiledTemplates.passwordChanged;
|
if (!isMailerConfigured()) return;
|
||||||
const emailContent = template({ windyCode });
|
|
||||||
|
const transporter = getTransporter();
|
||||||
|
const emailContent = compiledTemplates.passwordChanged({ windyCode });
|
||||||
|
|
||||||
const mailOptions = {
|
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,
|
to: email,
|
||||||
subject: '[Rusty Hearts] Account Password Changed',
|
subject: '[Rusty Hearts] Account Password Changed',
|
||||||
html: emailContent
|
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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');
|
import { Router } from 'express';
|
||||||
const router = express.Router();
|
const router = Router();
|
||||||
const net = require('net');
|
import { Socket } from 'net';
|
||||||
const { logger } = require('../utils/logger');
|
import config from '../config.js';
|
||||||
|
const { ports, ips, logger } = config;
|
||||||
|
import { create } from 'xmlbuilder2';
|
||||||
|
|
||||||
// Define the gateway route
|
// Constants
|
||||||
router.get('/', (req, res) => {
|
const SOCKET_TIMEOUT = 2000;
|
||||||
const ip = process.env.GATESERVER_IP;
|
const GATEWAY_STATUS = {
|
||||||
const port = process.env.GATESERVER_PORT || '50001';
|
ONLINE: 'online',
|
||||||
|
OFFLINE: 'offline'
|
||||||
|
};
|
||||||
|
|
||||||
// Generate the XML content with the IP and port values
|
/**
|
||||||
const xml = `<?xml version="1.0" encoding="ISO-8859-1"?>
|
* Build XML response for gateway info
|
||||||
<network>
|
*/
|
||||||
<gateserver ip="${ip}" port="${port}" />
|
function buildGatewayXml() {
|
||||||
</network>`;
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
res.set('Content-Type', 'application/xml');
|
/**
|
||||||
|
* Build gateway info route response
|
||||||
|
*/
|
||||||
|
function buildGatewayInfo(req) {
|
||||||
|
const baseUrl = `${req.protocol}://${req.headers.host}`;
|
||||||
|
return `1|${baseUrl}/launcher/GetGatewayAction|${baseUrl}/launcher/GetGatewayAction|`;
|
||||||
|
}
|
||||||
|
|
||||||
res.send(xml);
|
/**
|
||||||
});
|
* Check gateway server status via socket connection
|
||||||
|
*/
|
||||||
|
async function checkGatewayStatus() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const socket = new Socket();
|
||||||
|
socket.setTimeout(SOCKET_TIMEOUT);
|
||||||
|
|
||||||
// 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', () => {
|
socket.on('connect', () => {
|
||||||
logger.info(`[Gateway] Connection attempt success from IP: ${req.ip}`);
|
|
||||||
res.status(200).json({ status: 'online' });
|
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
resolve({ status: GATEWAY_STATUS.ONLINE });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('timeout', () => {
|
socket.on('timeout', () => {
|
||||||
logger.warn(`[Gateway] Connection attempt timeout from IP: ${req.ip}`);
|
|
||||||
res.status(408).json({ status: 'offline' });
|
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
resolve({ status: GATEWAY_STATUS.OFFLINE, code: 408 });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', () => {
|
socket.on('error', () => {
|
||||||
logger.error(`[Gateway] Connection failed from IP: ${req.ip}`);
|
|
||||||
res.status(503).json({ status: 'offline' });
|
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
resolve({ status: GATEWAY_STATUS.OFFLINE, code: 503 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.connect(ports.gate, ips.gate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main gateway route
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
// Gateway info route
|
||||||
|
router.get('/info', (req, res) => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gateway status route
|
||||||
|
router.get('/status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { status, code } = await checkGatewayStatus();
|
||||||
|
|
||||||
|
logger[status === GATEWAY_STATUS.ONLINE ? 'info' : 'warn'](
|
||||||
|
`[Gateway] Status check from ${req.ip}: ${status}`
|
||||||
|
);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -1,108 +1,60 @@
|
||||||
const sql = require('mssql');
|
import { Router } from "express";
|
||||||
const express = require('express');
|
const router = Router();
|
||||||
const router = express.Router();
|
import joi from "joi";
|
||||||
const bcrypt = require('bcryptjs');
|
import { logger, accountLogger } from "../../utils/logger.js";
|
||||||
const crypto = require('crypto');
|
import { changeAccountPassword } from "../../services/accountDBService.js";
|
||||||
const { logger, accountLogger } = require('../../utils/logger');
|
|
||||||
const { sendPasswordChangedEmail } = require('../../mailer/mailer');
|
|
||||||
const Joi = require('joi');
|
|
||||||
|
|
||||||
// Set up database connection
|
const schema = joi.object({
|
||||||
const { connAccount } = require('../../utils/dbConfig');
|
email: joi.string().email().required(),
|
||||||
|
password: joi.string().required(),
|
||||||
// Joi schema for request body validation
|
verificationCode: joi.string().pattern(new RegExp("^[0-9]+$")).required(),
|
||||||
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) => {
|
||||||
router.post('/', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
// Validate request body
|
// Validate request body
|
||||||
const { error, value } = schema.validate(req.body);
|
const { error, value } = schema.validate(req.body);
|
||||||
if (error) {
|
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 { email, password, verificationCode } = value;
|
||||||
const password = value.password;
|
|
||||||
const verificationCode = value.verification_code;
|
|
||||||
|
|
||||||
// Use a prepared statement to get the verification code
|
const changeAccountPasswordStatus = await changeAccountPassword(
|
||||||
const pool = await connAccount;
|
email,
|
||||||
const request = pool.request();
|
password,
|
||||||
request.input('Email', sql.VarChar, email);
|
verificationCode
|
||||||
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);
|
switch (changeAccountPasswordStatus) {
|
||||||
|
case "PasswordChanged":
|
||||||
// Use a prepared statement to update the password
|
accountLogger.info(
|
||||||
const pool = await connAccount;
|
`[Account] Account [${email}] password changed successfully`
|
||||||
const request = pool.request();
|
);
|
||||||
request.input('Email', sql.VarChar, email);
|
return res.status(200).json({
|
||||||
request.input('AccountPwd', sql.VarChar, passwordHash);
|
success: true,
|
||||||
const updateResult = await request.execute('UpdateAccountPassword');
|
result: changeAccountPasswordStatus,
|
||||||
const updateRow = updateResult.recordset[0];
|
});
|
||||||
|
default:
|
||||||
if (updateRow && updateRow.Result === 'PasswordChanged') {
|
accountLogger.info(
|
||||||
accountLogger.info(`[Account] Password for [${windyCode}] changed successfully`);
|
`[Account] Account [${email}] password change failed: ${changeAccountPasswordStatus}`
|
||||||
sendPasswordChangedEmail(email, windyCode);
|
);
|
||||||
|
return res.status(200).json({
|
||||||
const pool = await connAccount;
|
success: false,
|
||||||
const request = pool.request();
|
result: changeAccountPasswordStatus,
|
||||||
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) {
|
} catch (error) {
|
||||||
logger.error('[Account] A error ocourred: ' + error.message);
|
logger.error(`Account password change failed: ${error.message}`);
|
||||||
return res.status(500).send('A error ocourred. Please try again later.');
|
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');
|
import { Router } from "express";
|
||||||
const express = require('express');
|
const router = Router();
|
||||||
const router = express.Router();
|
import joi from "joi";
|
||||||
const { logger } = require('../../utils/logger');
|
import { logger } from "../../utils/logger.js";
|
||||||
const Joi = require('joi');
|
import { verifyCode } from "../../services/accountDBService.js";
|
||||||
|
|
||||||
// Set up database connection
|
|
||||||
const { connAccount } = require('../../utils/dbConfig');
|
|
||||||
|
|
||||||
// Joi schema for request body validation
|
// Joi schema for request body validation
|
||||||
const schema = Joi.object({
|
const schema = joi.object({
|
||||||
email: Joi.string().email().required(),
|
email: joi.string().email().required(),
|
||||||
verification_code_type: Joi.string().required(),
|
verificationCodeType: joi.string().required(),
|
||||||
verification_code: Joi.string().pattern(new RegExp('^[0-9]+$')).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 {
|
try {
|
||||||
// Validate request body
|
// Validate request body
|
||||||
const { error, value } = schema.validate(req.body);
|
const { error, value } = schema.validate(req.body);
|
||||||
if (error) {
|
if (error) {
|
||||||
return res.status(400).send(error.details[0].message);
|
return res.status(400).json({
|
||||||
}
|
success: false,
|
||||||
const email = req.body.email;
|
result: "ValidationError",
|
||||||
const verificationCode = req.body.verification_code;
|
message: error.details[0].message,
|
||||||
const verificationCodeType = req.body.verification_code_type;
|
});
|
||||||
|
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
||||||
return res.status(400).send('InvalidEmailFormat');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^\d+$/.test(verificationCode)) {
|
const {
|
||||||
return res.status(400).send('InvalidVerificationCodeFormat');
|
email,
|
||||||
|
verificationCode: verificationCode,
|
||||||
|
verificationCodeType: verificationCodeType,
|
||||||
|
} = value;
|
||||||
|
|
||||||
|
// 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(
|
||||||
// Use a prepared statement to check verification code
|
`[Account] Verification successful for ${email}. Status: ${verificationResult}`
|
||||||
const pool = await connAccount;
|
);
|
||||||
const request = pool.request();
|
return res.status(200).json({
|
||||||
request.input('Email', sql.VarChar, email);
|
success: true,
|
||||||
request.input('VerificationCode', sql.VarChar, verificationCode);
|
result: verificationResult,
|
||||||
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) {
|
} catch (error) {
|
||||||
logger.error('Database query failed: ' + error.message);
|
logger.error(`Verification failed: ${error.message}`);
|
||||||
return res.status(500).send('A error ocourred. Please try again later.');
|
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');
|
import { Router } from "express";
|
||||||
const fs = require('fs');
|
import { readFile, existsSync } from "fs";
|
||||||
const path = require('path');
|
import path from "path";
|
||||||
const { logger } = require('../../utils/logger');
|
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
|
// Endpoint to get the launcher version from the launcher_info.ini file
|
||||||
router.get('/getLauncherVersion', (req, res) => {
|
router.get("/getLauncherVersion", (req, res) => {
|
||||||
const launcherInfoPath = path.join(__dirname, '..', '..', '..', 'public', 'launcher', 'launcher_update', 'launcher_info.ini');
|
try {
|
||||||
fs.readFile(launcherInfoPath, 'utf8', (err, data) => {
|
const launcherInfoPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"public",
|
||||||
|
"launcher",
|
||||||
|
"launcher_update",
|
||||||
|
"launcher_info.ini"
|
||||||
|
);
|
||||||
|
|
||||||
|
readFile(launcherInfoPath, "utf8", (err, data) => {
|
||||||
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
logger.error(err);
|
||||||
return res.status(500).send('Error reading launcher_info.ini');
|
return res.status(400).json({
|
||||||
|
result: "FileReadError",
|
||||||
|
message: "Error reading launcher_info.ini file",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionRegex = /version=(.*)/i;
|
const versionRegex = /version=(.*)/i;
|
||||||
const match = data.match(versionRegex);
|
const match = data.match(versionRegex);
|
||||||
if (match) {
|
if (match) {
|
||||||
const launcherVersion = match[1];
|
const launcherVersion = match[1];
|
||||||
return res.json({ version: launcherVersion });
|
return res.status(200).json({
|
||||||
}
|
version: launcherVersion,
|
||||||
return res.status(500).send('Invalid launcher_info.ini format');
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Endpoint to download the new launcher version from the launcher_update folder
|
||||||
router.post('/updateLauncherVersion', (req, res) => {
|
router.post("/updateLauncherVersion", (req, res) => {
|
||||||
const launcherUpdatePath = path.join(__dirname, '..', '..', '..', 'public', 'launcher', 'launcher_update');
|
try {
|
||||||
|
const launcherUpdatePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"public",
|
||||||
|
"launcher",
|
||||||
|
"launcher_update"
|
||||||
|
);
|
||||||
|
|
||||||
const version = req.body.version;
|
const version = req.body.version;
|
||||||
if (!req.body.version) {
|
if (!version) {
|
||||||
return res.status(400).send('Missing version parameter');
|
return res.status(400).json({
|
||||||
}
|
result: "MissingVersion",
|
||||||
const file = path.join(launcherUpdatePath, `launcher_${version}.zip`);
|
message: "Missing version parameter",
|
||||||
if (!fs.existsSync(file)) {
|
});
|
||||||
return res.status(404).send(`File ${file} not found`);
|
}
|
||||||
logger.error(`[Launcher Updater] File ${file} not found`);
|
|
||||||
|
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');
|
import { Router } from "express";
|
||||||
const bcrypt = require('bcryptjs');
|
const router = Router();
|
||||||
const crypto = require('crypto');
|
import joi from "joi";
|
||||||
const express = require('express');
|
import config from "../../config.js";
|
||||||
const router = express.Router();
|
const { apiConfig } = config;
|
||||||
const { logger, accountLogger } = require('../../utils/logger');
|
import { logger } from "../../utils/logger.js";
|
||||||
const Joi = require('joi');
|
import { getClientIp } from "../../utils/getClientIp.js";
|
||||||
|
import { authenticateUser } from "../../services/accountDBService.js";
|
||||||
|
|
||||||
// Set up database connection
|
const schema = joi.object({
|
||||||
const { connAccount } = require('../../utils/dbConfig');
|
account: joi.string().required(),
|
||||||
|
password: joi.string().min(8).max(16).required(),
|
||||||
// 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) => {
|
router.post("/", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Validate the request body against the schema
|
// Validate request
|
||||||
const { error, value } = schema.validate(req.body);
|
const { error, value } = schema.validate(req.body);
|
||||||
if (error) {
|
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 { account, password } = value;
|
||||||
const password = req.body.password;
|
const ip = getClientIp(req);
|
||||||
const userIp = req.ip;
|
|
||||||
|
|
||||||
// Check the format of the account identifier
|
logger.info(
|
||||||
if (
|
apiConfig.logIPAddresses === "true"
|
||||||
!/^[a-z0-9_-]{6,50}$/.test(account) &&
|
? `[Launcher Login] Account [${account}] is trying to login from [${ip}]`
|
||||||
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account)
|
: `[Launcher Login] Account [${account}] is trying to login`
|
||||||
) {
|
|
||||||
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
|
|
||||||
);
|
);
|
||||||
|
// Authenticate user
|
||||||
|
const authResult = await authenticateUser(account, password, ip);
|
||||||
|
|
||||||
const authRequest = pool.request();
|
if (!authResult || authResult.status !== "LoginSuccess") {
|
||||||
authRequest.input('Identifier', sql.VarChar, account);
|
logger.warn(
|
||||||
authRequest.input(
|
`[Launcher Login] Authentication failed for user [${account}]: ${authResult?.status}`
|
||||||
'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({
|
return res.status(200).json({
|
||||||
Result: authRow.Result,
|
result: authResult?.status || "AuthenticationFailed",
|
||||||
Token: authRow.Token,
|
|
||||||
WindyCode: authRow.WindyCode,
|
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
accountLogger.info(
|
|
||||||
`[Account] Launcher Login: Account [${windyCode}] login failed: ${authRow.Result} `
|
logger.info(
|
||||||
|
`[Launcher Login] Authentication successful for user [${account}]`
|
||||||
);
|
);
|
||||||
return res.status(400).json({
|
return res.status(200).json({
|
||||||
Result: authRow.Result,
|
result: authResult.status,
|
||||||
|
token: authResult.token,
|
||||||
|
windyCode: account,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return res.status(400).json({ Result: 'AccountNotFound' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(`[Launcher Login] Authentication failed: ${error.message}`);
|
||||||
'[Account] Launcher Login: Database query failed: ' + error.message
|
return res.status(500).json({
|
||||||
);
|
result: "ServerError",
|
||||||
return res.status(500).send('Login failed. Please try again later.');
|
message: "A server error occurred. Please try again later.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,52 @@
|
||||||
const sql = require('mssql');
|
import { Router } from "express";
|
||||||
const express = require('express');
|
const router = Router();
|
||||||
const router = express.Router();
|
import { logger, accountLogger } from "../../utils/logger.js";
|
||||||
const { logger, accountLogger } = require('../../utils/logger');
|
import joi from "joi";
|
||||||
const { sendPasswordResetEmail } = require('../../mailer/mailer');
|
import { sendPasswordVerificationEmail } from "../../services/accountDBService.js";
|
||||||
const Joi = require('joi');
|
|
||||||
|
|
||||||
// Set up database connection
|
const schema = joi.object({
|
||||||
const { connAccount } = require('../../utils/dbConfig');
|
email: joi.string().email().required(),
|
||||||
|
|
||||||
// Joi schema for request body validation
|
|
||||||
const schema = Joi.object({
|
|
||||||
email: Joi.string().email().required()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route for registering an account
|
// Route for sending verification email for password reset
|
||||||
router.post('/', async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Validate request body
|
// Validate request body
|
||||||
const { error, value } = schema.validate(req.body);
|
const { error, value } = schema.validate(req.body);
|
||||||
if (error) {
|
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)) {
|
const sendEmailStatus = await sendPasswordVerificationEmail(email);
|
||||||
logger.info('Invalid email format');
|
|
||||||
return res.status(400).send('InvalidEmailFormat');
|
if (sendEmailStatus !== "Success") {
|
||||||
}
|
accountLogger.info(
|
||||||
|
`[Account] Password reset request failed to [${email}]. Status: ${sendEmailStatus}`
|
||||||
// Use a prepared statement to retrieve the account information
|
);
|
||||||
const pool = await connAccount;
|
return res.status(200).json({
|
||||||
const request = pool.request();
|
success: true,
|
||||||
request.input('Identifier', sql.VarChar, email);
|
result: sendEmailStatus,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
accountLogger.info(
|
||||||
|
`[Account] Password reset request sent to [${email}]. Status: ${sendEmailStatus}`
|
||||||
|
);
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
result: sendEmailStatus,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[Account] Database query failed: ' + error.message);
|
logger.error(`[Account] Password reset request failed: ${error.message}`);
|
||||||
return res.status(500).send('A error ocourred. Please try again later.');
|
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
|
|
@ -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
|
import { Router } from "express";
|
||||||
const env = require('../../utils/env');
|
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 schema = joi.object({
|
||||||
const express = require('express');
|
email: joi.string().email().required(),
|
||||||
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
|
// Route for sending verification email for account creation
|
||||||
router.post('/', async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Validate request body
|
// Validate request body
|
||||||
const { error, value } = schema.validate(req.body);
|
const { error, value } = schema.validate(req.body);
|
||||||
if (error) {
|
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)) {
|
const sendEmailStatus = await sendAccountVerificationEmail(email);
|
||||||
return res.status(400).send('InvalidEmailFormat');
|
|
||||||
}
|
if (sendEmailStatus !== "Success") {
|
||||||
|
accountLogger.info(
|
||||||
// Use a prepared statement to retrieve the account information
|
`[Account] Verification email sending failed to ${email}. Sending status: ${sendEmailStatus}`
|
||||||
const pool = await connAccount;
|
);
|
||||||
const request = pool.request();
|
return res.status(200).json({
|
||||||
request.input('Identifier', sql.VarChar, email);
|
success: false,
|
||||||
const result = await request.execute('GetAccount');
|
result: sendEmailStatus,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
accountLogger.info(
|
||||||
|
`[Account] Verification email sent to ${email}. Sending status: ${sendEmailStatus}`
|
||||||
|
);
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
result: sendEmailStatus,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[Account] Database query failed: ' + error.message);
|
logger.error(
|
||||||
return res.status(500).send('A error ocourred. Please try again later.');
|
`[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');
|
import { Router } from 'express';
|
||||||
const router = express.Router();
|
import sql from 'mssql';
|
||||||
const NodeCache = require('node-cache');
|
const router = Router();
|
||||||
const { logger } = require('../utils/logger');
|
import NodeCache from 'node-cache';
|
||||||
const sql = require('mssql');
|
import { logger } from '../utils/logger.js';
|
||||||
const { authDBConfig } = require('../utils/dbConfig.js');
|
import { authDBConfig } from '../utils/dbConfig.js';
|
||||||
|
import { fetchOnlineCount } from "../services/authDBService.js";
|
||||||
|
|
||||||
// Set up the cache
|
const CACHE_KEY = 'onlineCount';
|
||||||
const cache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
|
const CACHE_TTL = 60; // 1 minute
|
||||||
|
const CHECK_PERIOD = 120; // 2 minutes
|
||||||
|
|
||||||
// Route for getting the count of online players
|
// Cache configuration
|
||||||
router.get('/', async (req, res) => {
|
const cache = new NodeCache({
|
||||||
|
stdTTL: CACHE_TTL,
|
||||||
|
checkperiod: CHECK_PERIOD,
|
||||||
|
useClones: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Database connection pool
|
||||||
|
let pool;
|
||||||
|
let poolReady = (async () => {
|
||||||
try {
|
try {
|
||||||
// Check if the count exists in the cache
|
pool = await new sql.ConnectionPool(authDBConfig).connect();
|
||||||
const cacheKey = 'onlineCount';
|
} catch (error) {
|
||||||
let count = cache.get(cacheKey);
|
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) {
|
if (count === undefined) {
|
||||||
// Count not found in cache, fetch it from the database
|
logger.debug('Cache miss for online count, querying database');
|
||||||
const connAuth = new sql.ConnectionPool(authDBConfig);
|
count = await fetchOnlineCount(pool);
|
||||||
await connAuth.connect();
|
cache.set(CACHE_KEY, count);
|
||||||
|
} else {
|
||||||
const request = connAuth.request();
|
logger.debug('Online count retrieved from cache');
|
||||||
|
|
||||||
// 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 count;
|
||||||
return res.status(200).json({ count });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Database query failed: ' + error.message);
|
// If we have a cached value, return it even if the query fails
|
||||||
return res.status(500).send('Database query failed. Please try again later.');
|
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 {
|
||||||
|
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('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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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');
|
import sql from 'mssql';
|
||||||
const env = require('./env');
|
import { logger } from '../utils/logger.js';
|
||||||
const { logger } = require('../utils/logger');
|
|
||||||
|
|
||||||
const dbConfig = {
|
const dbConfig = {
|
||||||
user: process.env.DB_USER,
|
user: process.env.DB_USER,
|
||||||
|
|
@ -32,7 +31,7 @@ const authDBConfig = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
connAccount,
|
connAccount,
|
||||||
authDBConfig
|
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
|
|
@ -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
|
|
@ -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");
|
import dotenv from 'dotenv';
|
||||||
const winston = require("winston");
|
dotenv.config();
|
||||||
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
import { transports as _transports, format as _format, createLogger as _createLogger } from "winston";
|
||||||
|
|
||||||
const logsDirectory = 'logs';
|
const logsDirectory = 'logs';
|
||||||
|
|
||||||
if (!fs.existsSync(logsDirectory)) {
|
if (!existsSync(logsDirectory)) {
|
||||||
fs.mkdirSync(logsDirectory);
|
mkdirSync(logsDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLogger(filename, level, filter, showConsole) {
|
function createLogger(filename, level, filter, showConsole) {
|
||||||
const transports = [
|
const transports = [
|
||||||
new winston.transports.File({
|
new _transports.File({
|
||||||
filename: `${logsDirectory}/${filename}-${new Date().toISOString().slice(0, 10)}.log`,
|
filename: `${logsDirectory}/${filename}-${new Date().toISOString().slice(0, 10)}.log`,
|
||||||
level,
|
level,
|
||||||
filter
|
filter
|
||||||
|
|
@ -17,20 +19,20 @@ function createLogger(filename, level, filter, showConsole) {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (showConsole) {
|
if (showConsole) {
|
||||||
transports.push(new winston.transports.Console({
|
transports.push(new _transports.Console({
|
||||||
format: winston.format.combine(
|
format: _format.combine(
|
||||||
winston.format.colorize(),
|
_format.colorize(),
|
||||||
winston.format.simple(),
|
_format.simple(),
|
||||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
_format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = _createLogger({
|
||||||
level,
|
level,
|
||||||
format: winston.format.combine(
|
format: _format.combine(
|
||||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
_format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
_format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
|
||||||
),
|
),
|
||||||
transports
|
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 accountLogger = createLogger('account', logLevel, log => log.message.includes('[Account]'), process.env.LOG_ACCOUNT_CONSOLE === 'true');
|
||||||
const logger = createLogger('api', logLevel, null, true);
|
const logger = createLogger('api', logLevel, null, true);
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
authLogger,
|
authLogger,
|
||||||
billingLogger,
|
billingLogger,
|
||||||
mailerLogger,
|
mailerLogger,
|
||||||
|
|
|
||||||
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
|
|
@ -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
|
||||||
|
};
|
||||||
4
start-All.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
title Rusty Hearts API
|
||||||
|
node src/app.js mainApp usaApp jpnApp proxyServer
|
||||||
|
pause
|
||||||
4
start-JPN.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
title Rusty Hearts API (Jpn)
|
||||||
|
node src/app.js mainApp jpnApp proxyServer
|
||||||
|
pause
|
||||||
4
start-USA.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
title Rusty Hearts API (Usa)
|
||||||
|
node src/app.js mainApp usaApp
|
||||||
|
pause
|
||||||
4
start_with_pm2_All.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
title Rusty Hearts API
|
||||||
|
cmd /k "npx pm2 start ecosystem.config.cjs --only rh-api-all && npx pm2 logs rh-api"
|
||||||
|
pause
|
||||||
4
start_with_pm2_JPN.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
title Rusty Hearts API (Japan Server)
|
||||||
|
cmd /k "npx pm2 start ecosystem.config.cjs --only rh-api-jpn && npx pm2 logs rh-api"
|
||||||
|
pause
|
||||||
4
start_with_pm2_USA.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
title Rusty Hearts API (Usa Server)
|
||||||
|
cmd /k "npx pm2 start ecosystem.config.cjs --only rh-api-usa && npx pm2 logs rh-api"
|
||||||
|
pause
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
@echo off
|
|
||||||
title API
|
|
||||||
cmd /k "npx pm2 stop rh-api"
|
|
||||||
pause
|
|
||||||
4
stop_with_pm2.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
title API
|
||||||
|
cmd /k "npx pm2 stop all"
|
||||||
|
pause
|
||||||