Add things

This commit is contained in:
Christopher Cookman 2024-12-30 11:37:46 -07:00
commit 9d9f0e2e91
25 changed files with 3817 additions and 0 deletions

132
.gitignore vendored Normal file
View file

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

109
commands.js Normal file
View file

@ -0,0 +1,109 @@
// Command definitions
const Discord = require("discord.js")
module.exports = {
global: [
// Use SlashCommandBuilder for command creation
{
name: "give",
description: "Give a product to a user",
default_member_permissions: 0,
options: [
{
name: "product-name",
description: "The name of the product",
type: Discord.ApplicationCommandOptionType.String,
required: true
},
{
name: "roblox-id",
description: "The Roblox ID of the user",
type: Discord.ApplicationCommandOptionType.Number,
required: false
},
{
name: "discord-id",
description: "The Discord ID of the user",
type: Discord.ApplicationCommandOptionType.User,
required: false
}
]
},
{
name: "revoke",
description: "Revoke a product from a user",
default_member_permissions: 0,
options: [
{
name: "product-name",
description: "The name of the product",
type: Discord.ApplicationCommandOptionType.String,
required: true
},
{
name: "roblox-id",
description: "The Roblox ID of the user",
type: Discord.ApplicationCommandOptionType.Number,
required: false
},
{
name: "discord-id",
description: "The Discord ID of the user",
type: Discord.ApplicationCommandOptionType.User,
required: false
}
]
},
{
name: "link",
description: "Link your Roblox account",
options: [
{
name: "pairing-code",
description: "The pairing code given to you from the Roblox game",
type: Discord.ApplicationCommandOptionType.String,
required: true
}
]
},
{
name: "unlink",
description: "Unlink your Roblox account",
},
{
name: "products",
description: "List all products",
},
{
name: "profile",
description: "View a user's profile",
options: [
{
name: "roblox-id",
description: "The Roblox ID of the user",
type: Discord.ApplicationCommandOptionType.Number,
required: false
},
{
name: "discord-id",
description: "The Discord ID of the user",
type: Discord.ApplicationCommandOptionType.User,
required: false
}
]
},
{
name: "retrieve",
description: "Retrieve an owned product",
options: [
{
name: "product-name",
description: "The name of the product",
type: Discord.ApplicationCommandOptionType.String,
required: true
}
]
}
],
admin: []
}

36
commands/give.js Normal file
View file

@ -0,0 +1,36 @@
const client = global.discord_client
const pool = global.db_pool;
const execute = async (interaction) => {
var robloxID = interaction.options.getNumber("roblox-id") || null;
const discordID = interaction.options.getUser("discord-id")?.id || null;
if (!robloxID && !discordID) return interaction.reply({ content: "You must provide a Roblox ID or Discord ID", ephemeral: true });
// If both robloxID and discordID are provided, we can't continue
if (robloxID && discordID) return interaction.reply({ content: "You can't provide both a Roblox ID and Discord ID", ephemeral: true });
// If discordID is provided, we need to find the robloxID
if (discordID) {
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
if (!row) return interaction.reply({ content: "User not found", ephemeral: true });
robloxID = row.robloxId;
}
// Check if the user exists
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
const productName = interaction.options.getString("product-name");
// try catch try and find the product based on partial product name, parse everything in uppercase to make things easier
const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]);
if (!product) return interaction.reply({ content: "Product not found", ephemeral: true });
// Check if the user already owns the product
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
if (purchase) return interaction.reply({ content: "User already owns this product", ephemeral: true });
// Insert purchase into database
await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [robloxID, product.id, product.hubId]);
return interaction.reply({ content: `Gave \`${product.name}\` to ${robloxID}`, ephemeral: true });
};
module.exports = { execute }

15
commands/link.js Normal file
View file

@ -0,0 +1,15 @@
const client = global.discord_client
const pool = global.db_pool;
const execute = async (interaction) => {
const pairingCode = interaction.options.getString("pairing-code");
const discordID = interaction.user.id;
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
if (row) return interaction.reply({ content: "You have already linked your account", ephemeral: true });
const [pairing] = await pool.query('SELECT * FROM users WHERE pairingCode = ?', [pairingCode]);
if (!pairing) return interaction.reply({ content: "Invalid pairing code", ephemeral: true });
await pool.query('UPDATE users SET discordId = ?, discordDisplayName = ?, pairingCode = NULL WHERE pairingCode = ?', [discordID, interaction.user.displayName, pairingCode]);
return interaction.reply({ content: "Successfully linked your account", ephemeral: true });
}
module.exports = { execute }

24
commands/products.js Normal file
View file

@ -0,0 +1,24 @@
const client = global.discord_client
const pool = global.db_pool;
const execute = async (interaction) => {
try {
const [hub] = await pool.query(`SELECT * FROM hubs WHERE discordGuild = ?`, [interaction.guildId])
if (!hub) return interaction.reply({ content: "This server doesn't have a hub", ephemeral: true });
const products = await pool.query(`SELECT * FROM products WHERE hubId = ?`, [hub.id]);
const embed = {
title: hub.name,
fields: products.map(product => ({
name: product.name,
value: product.description
})),
color: 0x2F3136
}
return interaction.reply({ embeds: [embed] });
} catch (error) {
console.error(error);
return interaction.reply({ content: "An error occurred while fetching products", ephemeral: true });
}
};
module.exports = { execute }

44
commands/profile.js Normal file
View file

@ -0,0 +1,44 @@
const client = global.discord_client
const pool = global.db_pool;
const execute = async (interaction) => {
var robloxID = interaction.options.getNumber("roblox-id") || null;
const discordID = interaction.options.getUser("discord-id")?.id || null;
if (!robloxID && !discordID) return interaction.reply({ content: "You must provide a Roblox ID or Discord ID", ephemeral: true });
// If both robloxID and discordID are provided, we can't continue
if (robloxID && discordID) return interaction.reply({ content: "You can't provide both a Roblox ID and Discord ID", ephemeral: true });
// If discordID is provided, we need to find the robloxID
if (discordID) {
const [user] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
robloxID = user.robloxId;
}
// Check if the user exists
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
// Check if the user already owns the product
const purchases = await pool.query('SELECT * FROM purchases WHERE robloxId = ?', [robloxID]);
const products = await pool.query('SELECT * FROM products');
const embed = {
title: `Profile for ${user.discordDisplayName}`,
description: `Roblox ID: ${robloxID}`,
fields: [
{
name: `Owned Products (${purchases.length})`,
value: purchases.map(purchase => {
const product = products.find(product => product.id === purchase.productId);
return product.name;
}).join("\n")
}
]
}
return interaction.reply({ embeds: [embed] });
};
module.exports = { execute }

57
commands/retrieve.js Normal file
View file

@ -0,0 +1,57 @@
const client = global.discord_client
const pool = global.db_pool;
const crypto = require('crypto');
const execute = async (interaction) => {
// If discordID is provided, we need to find the robloxID
const [user] = await pool.query('SELECT * FROM users WHERE discordId = ?', [interaction.user.id]);
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
robloxID = user.robloxId;
// Check if the user exists
if (!robloxID) return interaction.reply({ content: "User not found", ephemeral: true });
const productName = interaction.options.getString("product-name");
// try catch try and find the product based on partial product name, parse everything in uppercase to make things easier
const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]);
if (!product) return interaction.reply({ content: "Product not found", ephemeral: true });
// Check if the user already owns the product
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
if (!purchase) return interaction.reply({ content: `You don't own \`${product.name}\``, ephemeral: true });
const [auth] = await pool.query('SELECT * FROM fileAuth WHERE owner = ? and product = ?', [robloxID, product.id]);
if (auth && auth.expires > Date.now()) return interaction.reply({ content: `Download \`${product.name}\``, ephemeral: true, components: [
{
type: 1,
components: [
{
type: 2,
label: 'Download',
style: 5,
url: `https://cdn.example.com/${product.file}/${authToken}`
}
]
}
] });
// Use crypto to generate 32 character auth token
const authToken = crypto.randomBytes(16).toString('hex');
// Insert the new auth token into the fileAuth table with an expiration date of 24 hours from now
await pool.query('INSERT INTO fileAuth (owner, product, token, expires) VALUES (?, ?, ?, ?)', [robloxID, product.id, authToken, Date.now() + 24 * 60 * 60 * 1000]);
// Reply with the download link
return interaction.reply({ content: `Download \`${product.name}\``, ephemeral: true, components: [
{
type: 1,
components: [
{
type: 2,
label: 'Download',
style: 5,
url: `https://cdn.example.com/${product.file}/${authToken}`
}
]
}
] });
};
module.exports = { execute }

36
commands/revoke.js Normal file
View file

@ -0,0 +1,36 @@
const client = global.discord_client
const pool = global.db_pool;
const execute = async (interaction) => {
var robloxID = interaction.options.getNumber("roblox-id") || null;
const discordID = interaction.options.getUser("discord-id")?.id || null;
if (!robloxID && !discordID) return interaction.reply({ content: "You must provide a Roblox ID or Discord ID", ephemeral: true });
// If both robloxID and discordID are provided, we can't continue
if (robloxID && discordID) return interaction.reply({ content: "You can't provide both a Roblox ID and Discord ID", ephemeral: true });
// If discordID is provided, we need to find the robloxID
if (discordID) {
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
if (!row) return interaction.reply({ content: "User not found", ephemeral: true });
robloxID = row.robloxId;
}
// Check if the user exists
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
const productName = interaction.options.getString("product-name");
// try catch try and find the product based on partial product name, parse everything in uppercase to make things easier
const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]);
if (!product) return interaction.reply({ content: "Product not found", ephemeral: true });
// Check if the user already owns the product
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
if (!purchase) return interaction.reply({ content: "User doesn't own product", ephemeral: true });
// Remove purchase from database
await pool.query('DELETE FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
return interaction.reply({ content: `Removed \`${product.name}\` from ${robloxID}`, ephemeral: true });
};
module.exports = { execute }

12
commands/unlink.js Normal file
View file

@ -0,0 +1,12 @@
const client = global.discord_client
const pool = global.db_pool;
const execute = async (interaction) => {
const discordID = interaction.user.id;
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
if (!row) return interaction.reply({ content: "Your account isn't linked!", ephemeral: true });
await pool.query('UPDATE users SET discordId = NULL, pairingCode = NULL WHERE discordId = ?', [discordID]);
return interaction.reply({ content: "Successfully unlinked your account. Please use the hub game to get a new pairing code!", ephemeral: true });
}
module.exports = { execute }

105
index.js Normal file
View file

@ -0,0 +1,105 @@
require("dotenv").config();
const MariaDB = require('mariadb');
const fs = require('fs');
const express = require('express');
const path = require('path');
const log = require("./log")
const app = express();
global.log = log;
const Discord = require("discord.js")
const client = new Discord.Client({intents: ["Guilds", "DirectMessages"]})
const pool = MariaDB.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
connectionLimit: 50 // Overkill but why not lol
});
global.db_pool = pool; // For access in other files
// Middleware to parse incoming JSON requests
app.use(express.json());
// Middleware to log requests
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production') return next();
log.info('--- Incoming Request ---');
log.info(`Method: ${req.method}`);
log.info(`URL: ${req.url}`);
log.info('Headers:', req.headers);
if (Object.keys(req.body).length > 0) {
log.info('Body:', req.body);
}
log.info('------------------------');
next();
});
global.discord_client = client
client.on("ready", async () => {
log.info(`Logged into Discord as ${client.user.displayName}`);
const commands = require("./commands")
// Command registration
log.info("Registering commands...")
await (async () => {
try {
const rest = new Discord.REST().setToken(client.token);
//Global
//await rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: [] })
log.info(`Registering global commands`);
rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands.global }).then(() => {
log.info("Global commands registered")
}).catch((error) => {
log.error(error)
});
//Admin
// rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: commands.admin }).then(() => {
// log.info("Admin commands registered")
// }).catch((error) => {
// log.error(error)
// });
} catch (error) {
log.error(error)
}
})();
app.listen(port, () => {
log.info(`Listening on ${port}`)
})
});
client.on("interactionCreate", async (interaction) => {
if (!interaction.isCommand()) return;
const command = require(`./commands/${interaction.commandName}`);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
log.error(error.stack);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
});
const port = process.env.SERVER_PORT || 3000;
require("./migrations")(pool)
.finally(() => {
// Load all route modules from the 'routes' folder
const routesPath = path.join(__dirname, 'routes');
fs.readdirSync(routesPath).forEach((file) => {
const route = require(path.join(routesPath, file));
const routeName = `/${file.replace('.js', '')}`; // Use filename as route base
app.use(routeName, route);
log.info(`Using ${routeName}`)
});
client.login(process.env.DISCORD_TOKEN);
});

9
log.js Normal file
View file

@ -0,0 +1,9 @@
const colors = require("colors");
module.exports = {
info(msg) {
console.log(`${colors.cyan.bold("[INFO]")} ${msg}`);
},
error(msg) {
console.log(`${colors.red.bold("[ERROR]")} ${msg}`);
}
}

72
migrations.js Normal file
View file

@ -0,0 +1,72 @@
const mariadb = require('mariadb');
const fs = require('fs');
const path = require('path');
const util = require("util")
const log = require("./log")
function runMigrations(pool) {
return new Promise((resolve, reject) => {
let connection;
pool.getConnection()
.then(conn => {
connection = conn;
// Ensure a migrations table exists to track applied migrations
return connection.query(`CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`);
})
.then(() => {
// Read all migration files
const migrationDir = path.join(__dirname, 'migrations');
const files = fs.readdirSync(migrationDir).sort(); // Sort to apply in order
return files.reduce((promise, file) => {
return promise.then(() => {
const migrationName = path.basename(file);
// Check if the migration has already been applied
return connection.query(
'SELECT 1 FROM migrations WHERE name = ? LIMIT 1',
[migrationName]
).then(([rows]) => {
if (Object.keys(rows || {}).length > 0) {
//console.log(`Skipping already applied migration: ${migrationName}`);
return; // Skip this migration
}
// Read and execute the migration SQL
const migrationPath = path.join(migrationDir, file);
const sql = fs.readFileSync(migrationPath, 'utf8');
return connection.query(sql).then(() => {
// Record the applied migration
return connection.query(
'INSERT INTO migrations (name) VALUES (?)',
[migrationName]
).then(() => {
log.info(`Applied migration: ${migrationName}`);
});
});
});
});
}, Promise.resolve());
})
.then(() => {
log.info('All migrations applied successfully!');
resolve();
})
.catch(err => {
log.error('Error running migrations:', err);
reject(err);
})
.finally(() => {
if (connection) connection.release();
});
});
}
module.exports = runMigrations

View file

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS users (
robloxId BIGINT PRIMARY KEY,
discordId BIGINT DEFAULT NULL,
pairingCode VARCHAR(6) DEFAULT NULL,
discordDisplayName VARCHAR(48) DEFAULT 'Unknown',
UNIQUE (robloxId, discordId)
)

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS hubs (
id CHAR(36) PRIMARY KEY DEFAULT UUID(),
ownerId BIGINT, -- Owner of the hub (Discord ID)
discordGuild BIGINT, -- Discord Guild ID
name VARCHAR(128), -- Name of the hub
shortDescription VARCHAR(256), -- Short description of the hub
longDescription TEXT, -- Long description of the hub
allowGiftPurchase BOOLEAN DEFAULT TRUE, -- Allow users to buy gifts for others on this hub.
tos TEXT DEFAULT 'This Hub does not have any Terms of Service yet. If you are the Hub owner, you can update this under settings.', -- Terms of Service
bgmId BIGINT DEFAULT NULL, -- Background Music ID
secretKey VARCHAR(128) UNIQUE NOT NULL -- Secret key for the hub to authenticate
)

View file

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS products (
id CHAR(36) PRIMARY KEY DEFAULT UUID(),
hubId CHAR(36), -- Hub that the product belongs to
name VARCHAR(128), -- Name of the product
description TEXT, -- Description of the product
devProductID BIGINT NOT NULL, -- Dev Product ID
decalId BIGINT DEFAULT NULL, -- Decal ID
stock BIGINT DEFAULT -1, -- Stock of the product (If enabled)
category VARCHAR(64) DEFAULT NULL, -- Category of the product
file VARCHAR(36) DEFAULT NULL, -- File ID for the product
fileType VARCHAR(16) DEFAULT NULL, -- File type of the product
FOREIGN KEY (hubId) REFERENCES hubs(id)
)

View file

@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS purchases (
id CHAR(36) PRIMARY KEY DEFAULT UUID(),
robloxId BIGINT, -- User that made the purchase
productId CHAR(36), -- Product that was purchased
hubId CHAR(36), -- Hub that the product belongs to
FOREIGN KEY (robloxId) REFERENCES users(robloxId),
FOREIGN KEY (productId) REFERENCES products(id),
FOREIGN KEY (hubId) REFERENCES hubs(id),
UNIQUE (productId, robloxId)
)

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS fileAuth (
token CHAR(64) PRIMARY KEY NOT NULL, -- Single use token to download the file
product VARCHAR(36) NOT NULL, -- Product ID (Reference to product in products table)
owner BIGINT NOT NULL, -- Owner of the token
expires BIGINT NOT NULL, -- Expiration for this token
FOREIGN KEY (owner) REFERENCES users(robloxId),
FOREIGN KEY (product) REFERENCES products(id)
)

View file

@ -0,0 +1,4 @@
-- Insert a user
INSERT INTO users (robloxId, discordId, pairingCode, discordDisplayName)
VALUES (25226480, 289884287765839882, NULL, 'Chris C');

View file

@ -0,0 +1,3 @@
-- Insert a hub
INSERT INTO hubs (ownerId, discordGuild, name, shortDescription, longDescription, allowGiftPurchase, tos, bgmId, secretKey)
VALUES (289884287765839882, 1271619282853429330, 'Test Hub', 'A test hub', 'A test hub', TRUE, 'This Hub does not have any Terms of Service yet. If you are the Hub owner, you can update this under settings.', NULL, 'test_hub');

View file

@ -0,0 +1,3 @@
-- insert test product
INSERT INTO products (hubId, name, description, devProductID, decalId, stock, category)
VALUES ((SELECT id FROM hubs WHERE name = 'Test Hub'), 'Test Product', 'A test product', 1, NULL, -1, 'Test');

2879
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

23
package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "parcel_selfhost",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.7.8",
"colors": "^1.4.0",
"discord.js": "^14.16.3",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"express-session": "^1.18.1",
"mariadb": "^3.4.0",
"passport": "^0.7.0",
"passport-discord": "^0.1.4",
"sqlite3": "^5.1.7"
}
}

32
routes/cdn.js Normal file
View file

@ -0,0 +1,32 @@
const express = require('express');
const router = express.Router();
const pool = global.db_pool;
const log = global.log;
const fs = require('fs');
const path = require('path');
router.get("/:fileId/:authToken", async (req, res) => {
const { fileId, authToken } = req.params;
const [auth] = await pool.query('SELECT * FROM fileAuth WHERE token = ?', [authToken]);
if (!auth) return res.status(403).send("Invalid token");
const [product] = await pool.query('SELECT * FROM products WHERE file = ? AND id = ?', [fileId, auth.product]);
if (!auth || !product) return res.status(404).send("File not found");
log.info("Auth and product found")
if (auth.expires < Date.now()) {
res.status(403).send("Token expired");
return pool.query('DELETE FROM fileAuth WHERE token = ?', [authToken]);
}
const safeFileId = path.basename(product.file);
const filePath = path.join(__dirname, '../productFiles', safeFileId);
if (!fs.existsSync(filePath)) return res.status(404).send("File not found");
log.info("File exists!")
res.setHeader('Content-Disposition', `attachment; filename=${product.file}.${product.fileType}`);
res.sendFile(filePath, (err) => {
if (err) {
log.error(`Error sending file: ${err}`);
res.status(500).send("Error sending file");
}
});
})
module.exports = router;

93
routes/hub.js Normal file
View file

@ -0,0 +1,93 @@
const express = require('express');
const router = express.Router();
const pool = global.db_pool;
const log = global.log;
router.get('/getSession', async (req, res) => {
if (!req.headers.authorization) return res.status(401).json({ status: 401, message: 'Missing Authorization Header', data: {} });
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers.authorization]);
if (!hub) return res.status(401).json({ status: 401, message: 'Invalid Authorization Header', data: {} });
try {
const pid = req.query.robloxPlayerId;
if (!pid) return res.status(400).json({ status: 400, message: 'Missing Roblox Player ID' });
const [row] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [pid]);
if (!row) {
const pairingCode = Math.floor(100000 + Math.random() * 900000).toString();
await pool.query('INSERT INTO users (robloxId, pairingCode) VALUES (?, ?)', [pid, pairingCode]);
return res.status(409).json({ status: 409, message: `User is not paired! Run /link ${pairingCode} on Discord!`, pairingCode, data: {} });
} else if (!row.discordId && !row.pairingCode) {
const pairingCode = Math.floor(100000 + Math.random() * 900000).toString();
await pool.query('UPDATE users SET pairingCode = ? WHERE robloxId = ?', [pairingCode, pid]);
return res.status(409).json({ status: 409, message: `User is not paired! Run /link ${pairingCode} on Discord!`, pairingCode, data: {} });
} else if (!row.discordId && row.pairingCode !== null) {
return res.status(409).json({ status: 409, message: `User is not paired! Run /link ${row.pairingCode} on Discord!`, data: {} });
} else {
try {
const products = await pool.query('SELECT * FROM products WHERE hubId = ?', [hub.id]);
const purchases = await pool.query('SELECT * FROM purchases WHERE hubId = ?', [hub.id]);
// generate array of products
const respData = JSON.stringify({
status: 200,
message: "OK",
data: {
userData: {
connectedUsername: row.discordDisplayName,
},
hubData: {
id: hub.id,
name: hub.name,
description: {
shortDescription: hub.shortDescription,
longDescription: hub.longDescription,
},
config: {
allowGiftPurchases: hub.allowGiftPurchase ? true : false,
},
statistics: {
totalSales: purchases.length,
},
regulatory: {
termsOfService: hub.tos
},
},
productsData: {
allProducts: products.map(product => ({
productID: product.id,
name: product.name,
description: product.description,
devproduct_id: product.devProductID,
decalID: product.decalId || "0",
stock: product.stock > 0 ? product.stock : false,
onsale: true,
category: product.category,
rating: {
currentScore: 0,
maxScore: 0,
amountOfReviews: 0
},
tags: [],
playerData: {
robloxId: pid,
ownsProduct: purchases.some(purchase => purchase.productId == product.id && purchase.robloxId == pid)
}
})),
},
robloxGame: {
musicId: hub.musicId,
},
requestedAt: new Date().toISOString(),
}
}, (_, v) => typeof v === 'bigint' ? v.toString() : v)
return res.status(200).send(respData)
} catch (error) {
log.error(error);
return res.status(500).json({ status: 500, message: 'Internal Server Error', data: {} });
}
}
} catch (error) {
log.error(error);
return res.status(500).json({ status: 500, message: 'Internal Server Error', data: {} });
}
});
module.exports = router;

79
routes/payments.js Normal file
View file

@ -0,0 +1,79 @@
const express = require('express');
const router = express.Router();
const pool = global.db_pool;
// Main payment processor
router.post("/external/hub/order/complete", async (req, res) => {
// Get hub and validate secret
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers["hub-secret-key"]]);
if (!hub) return res.status(404).json({ status: "404", message: 'Invalid Authorization Header' });
// Get Roblox ID and Product ID
const { robloxID, productID } = req.body;
if (!robloxID || !productID) return res.status(400).json({ status: "400", message: 'Missing Roblox ID or Product ID' });
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
const [product] = await pool.query('SELECT * FROM products WHERE id = ?', [productID]);
// Check if user and product exists
if (!user || !product) return res.status(404).json({ status: "404", message: 'User or Product not found' });
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
// Check if purchase already exists
if (purchase) return res.status(200).json({ status: "200", message: 'Purchase already exists' });
// Insert purchase into database
await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [robloxID, product.id, hub.id]);
});
// Gift validator
router.post("/external/hub/gift/validate", async (req, res) => {
// Get hub and validate secret
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers["hub-secret-key"]]);
if (!hub) return res.status(404).json({ status: "404", message: 'Invalid Authorization Header' });
// Get Roblox ID and Product ID
const { recipientID, productID } = req.body;
if (!recipientID || !productID) return res.status(400).json({ status: "400", message: 'Missing Roblox ID or Product ID' });
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [recipientID]);
const [product] = await pool.query('SELECT * FROM products WHERE id = ?', [productID]);
// Check that the product exists
if (!product) return res.status(404).json({ status: "404", message: 'Product not found', data: {} });
// Check if user exists, if not create a new user
if (!user) return res.status(404).json({ status: "404", message: 'User not found', data: {userExists: false} });
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [recipientID, product.id]);
// Check if purchase already exists
if (purchase) return res.status(409).json({ status: "409", message: 'User already owns product', data: {userExists: true, ownsProduct: true} });
// All good!
return res.status(200).json({ status: "200", message: 'User does not own product', data: {userExists: true, ownsProduct: false} });
});
// Gift processor
router.post("/external/hub/gift/complete", async (req, res) => {
// Get hub and validate secret
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers["hub-secret-key"]]);
if (!hub) return res.status(404).json({ status: "404", message: 'Invalid Authorization Header' });
// Get Roblox ID and Product ID
const { recipientID, productID } = req.body;
if (!recipientID || !productID) return res.status(400).json({ status: "400", message: 'Missing Roblox ID or Product ID' });
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [recipientID]);
const [product] = await pool.query('SELECT * FROM products WHERE id = ?', [productID]);
// Check if user and product exists
if (!user || !product) return res.status(404).json({ status: "404", message: 'User or Product not found' });
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [recipientID, product.id]);
// Check if purchase already exists
if (purchase) return res.status(200).json({ status: "200", message: 'Purchase already exists' });
// Insert purchase into database
await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [recipientID, product.id, hub.id]);
});
module.exports = router;