From 262d2ece2b39b8b67f9e366c9c753d1207149b2d Mon Sep 17 00:00:00 2001 From: Govorunb Date: Thu, 20 Jun 2024 02:13:37 +1000 Subject: [PATCH 1/9] uncringe db schema part 1 --- common/types.ts | 18 +++++-- ebs/src/index.ts | 4 +- ebs/src/modules/config.ts | 9 ---- .../index.ts} | 46 +++++++--------- ebs/src/modules/transactions/user.ts | 47 ++++++++++++++++ ebs/src/util/db.ts | 53 ++++++++++++++----- ebs/src/util/middleware.ts | 19 +++++-- frontend/www/src/modules/auth.ts | 33 ++++++++++-- frontend/www/src/modules/pubsub.ts | 27 ++++++---- frontend/www/src/util/config.ts | 31 +++++------ 10 files changed, 196 insertions(+), 91 deletions(-) rename ebs/src/modules/{transactions.ts => transactions/index.ts} (90%) create mode 100644 ebs/src/modules/transactions/user.ts diff --git a/common/types.ts b/common/types.ts index 95c4a43..339ac70 100644 --- a/common/types.ts +++ b/common/types.ts @@ -68,7 +68,6 @@ export type Config = { version: number; enums?: { [name: string]: string[] }; redeems?: { [id: string]: Redeem }; - banned?: string[]; message?: string; }; @@ -88,10 +87,23 @@ export type Transaction = { token: string; }; -export type PubSubMessage = { - type: string; +export type PubSubMessage = ConfigRefreshed | Banned; +export type PubSubMessageBase = { + type: "config_refreshed" | "banned"; data: string; }; +export type ConfigRefreshed = PubSubMessageBase & { + type: "config_refreshed"; + data: string; +} +export type Banned = PubSubMessageBase & { + type: "banned"; + data: string; +} +export type BannedData = { + id: string; + banned: boolean; +} export type LogMessage = { transactionToken: string | null; diff --git a/ebs/src/index.ts b/ebs/src/index.ts index 8eb6c8a..4548877 100644 --- a/ebs/src/index.ts +++ b/ebs/src/index.ts @@ -3,7 +3,7 @@ import cors from "cors"; import express from "express"; import expressWs from "express-ws"; import bodyParser from "body-parser"; -import { privateApiAuth, publicApiAuth } from "./util/middleware"; +import { asyncCatch, privateApiAuth, publicApiAuth } from "./util/middleware"; import { initDb } from "./util/db"; import { sendToLogger } from "./util/logger"; @@ -14,7 +14,7 @@ const port = 3000; export const { app } = expressWs(express()); app.use(cors({ origin: "*" })); app.use(bodyParser.json()); -app.use("/public/*", publicApiAuth); +app.use("/public/*", asyncCatch(publicApiAuth)); app.use("/private/*", privateApiAuth); app.get("/", (_, res) => { diff --git a/ebs/src/modules/config.ts b/ebs/src/modules/config.ts index 9ff9113..c04f486 100644 --- a/ebs/src/modules/config.ts +++ b/ebs/src/modules/config.ts @@ -2,7 +2,6 @@ import { Config } from "common/types"; import { app } from ".."; import { sendPubSubMessage } from "../util/pubsub"; import { compressSync, strFromU8, strToU8 } from "fflate"; -import { getBannedUsers } from "../util/db"; import { asyncCatch } from "../util/middleware"; import { Webhooks } from "@octokit/webhooks"; import { sendToLogger } from "../util/logger"; @@ -22,10 +21,6 @@ async function fetchConfig(): Promise { const data: Config = JSON.parse(atob(responseData.content)) - console.log(data); - - data.banned = await getBannedUsers(); - return data; } catch (e: any) { console.error("Error when fetching config from api URL, falling back to raw URL"); @@ -48,10 +43,6 @@ async function fetchConfig(): Promise { const response = await fetch(url); const data: Config = await response.json(); - console.log(data) - - data.banned = await getBannedUsers(); - return data; } catch (e: any) { console.error("Error when fetching config from raw URL, panic"); diff --git a/ebs/src/modules/transactions.ts b/ebs/src/modules/transactions/index.ts similarity index 90% rename from ebs/src/modules/transactions.ts rename to ebs/src/modules/transactions/index.ts index 565d697..db861d1 100644 --- a/ebs/src/modules/transactions.ts +++ b/ebs/src/modules/transactions/index.ts @@ -1,31 +1,23 @@ import { Cart, Config, LogMessage, Transaction } from "common/types"; -import { app } from ".."; -import { parseJWT, verifyJWT } from "../util/jwt"; -import { BitsTransactionPayload } from "../types"; -import { getConfig } from "./config"; -import { addFulfilledTransaction, deletePrepurchase, getPrepurchase, isReceiptUsed, isUserBanned, registerPrepurchase } from "../util/db"; -import { sendToLogger } from "../util/logger"; -import { connection } from "./game"; -import { TwitchUser } from "./game/messages"; -import { asyncCatch } from "../util/middleware"; -import { sendShock } from "../util/pishock"; -import { getTwitchUser } from "./twitch"; +import { app } from "../.."; +import { parseJWT, verifyJWT } from "../../util/jwt"; +import { BitsTransactionPayload } from "../../types"; +import { getConfig } from "../config"; +import { addFulfilledTransaction, deletePrepurchase, getPrepurchase, isReceiptUsed, registerPrepurchase } from "../../util/db"; +import { sendToLogger } from "../../util/logger"; +import { connection } from "../game"; +import { TwitchUser } from "../game/messages"; +import { asyncCatch } from "../../util/middleware"; +import { sendShock } from "../../util/pishock"; +import { getTwitchUser } from "../twitch"; + +require('./user'); app.post( "/public/prepurchase", asyncCatch(async (req, res) => { const cart = req.body as Cart; - const idCart = { ...cart, userId: req.twitchAuthorization!.user_id! }; - - if (await isUserBanned(req.twitchAuthorization!.user_id!)) { - res.status(403).send("You are banned from using this extension."); - return; - } - - if (await isUserBanned(req.twitchAuthorization!.opaque_user_id!)) { - res.status(403).send("You are banned from using this extension."); - return; - } + const idCart = { ...cart, userId: req.user.id }; if (!connection.isConnected()) { res.status(502).send("Game connection is not available"); @@ -104,7 +96,7 @@ app.post( const logContext: LogMessage = { transactionToken: transaction.token, - userIdInsecure: req.twitchAuthorization!.user_id!, + userIdInsecure: req.user.id, important: true, fields: [ { @@ -151,13 +143,13 @@ app.post( return; } - await addFulfilledTransaction(transaction.receipt, transaction.token, req.twitchAuthorization!.user_id!); + await addFulfilledTransaction(transaction.receipt, transaction.token, req.user.id!); - if (cart.userId != req.twitchAuthorization!.user_id!) { + if (cart.userId != req.user.id) { logContext.important = true; logMessage.header = "Mismatched user ID"; logMessage.content = { - auth: req.twitchAuthorization, + user: req.user, cart, transaction, }; @@ -270,7 +262,7 @@ app.post( } catch (error) { sendToLogger({ transactionToken: token, - userIdInsecure: req.twitchAuthorization!.user_id!, + userIdInsecure: req.user.id, important: false, fields: [ { diff --git a/ebs/src/modules/transactions/user.ts b/ebs/src/modules/transactions/user.ts new file mode 100644 index 0000000..d72534f --- /dev/null +++ b/ebs/src/modules/transactions/user.ts @@ -0,0 +1,47 @@ +import { app } from "../.."; +import { getUser, saveUser } from "../../util/db"; +import { asyncCatch } from "../../util/middleware"; +import { sendPubSubMessage } from "../../util/pubsub"; + +export type User = { + id: string, + login?: string, + displayName?: string, + credit: number, + banned: boolean, +} + +export async function setUserBanned(id: string, banned: boolean) { + const user = await getUser(id); + user.banned = banned; + await saveUser(user); + await sendPubSubMessage({ + type: "banned", + data: JSON.stringify({id, banned}), + }); +} + +app.post("/public/authorized", asyncCatch(async (req, res) => { + res.sendStatus(200); +})) + +app.post("/private/ban/:id", asyncCatch(async (req, res) => { + const id = req.params["id"]; + await setUserBanned(id, true); + res.sendStatus(200); +})) + +app.delete("/private/ban/:id", asyncCatch(async (req, res) => { + const id = req.params["id"]; + await setUserBanned(id, false); + res.sendStatus(200); +})) + +app.post("/private/credit/:id", asyncCatch(async (req, res) => { + const id = req.params["id"]; + const amount = req.body.amount as number; + const user = await getUser(id); + user.credit += amount; + await saveUser(user); + res.sendStatus(200); +})) \ No newline at end of file diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index a4d44d8..5b7ae76 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -2,6 +2,7 @@ import { RowDataPacket } from "mysql2"; import mysql from "mysql2/promise"; import { IdentifiableCart } from "common/types"; import { v4 as uuid } from "uuid"; +import { User } from "../modules/transactions/user"; export let db: mysql.Connection; @@ -17,6 +18,7 @@ export async function initDb() { user: process.env.MYSQL_USER, password: process.env.MYSQL_PASSWORD, database: process.env.MYSQL_DATABASE, + namedPlaceholders: true, }); } catch { console.log("Failed to connect to database. Retrying in 5 seconds..."); @@ -28,6 +30,15 @@ export async function initDb() { } export async function setupDb() { + await db.query(` + CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(255) PRIMARY KEY, + login VARCHAR(255), + displayName VARCHAR(255), + credit INT, + banned BOOLEAN + ); + `); await db.query(` CREATE TABLE IF NOT EXISTS transactions ( receipt VARCHAR(1024) PRIMARY KEY, @@ -44,12 +55,6 @@ export async function setupDb() { ); `); - await db.query(` - CREATE TABLE IF NOT EXISTS bans ( - userId VARCHAR(255) PRIMARY KEY - ); - `); - await db.query(` CREATE TABLE IF NOT EXISTS logs ( id INT PRIMARY KEY AUTO_INCREMENT, @@ -118,23 +123,43 @@ export async function deletePrepurchase(token: string) { } } -export async function isUserBanned(userId: string): Promise { +const emptyUser: User = { + id: "", + credit: 0, + banned: false +}; + +export async function getUser(id: string): Promise { try { - const [rows] = (await db.query("SELECT COUNT(*) FROM bans WHERE userId = ?", [userId])) as [RowDataPacket[], any]; - return rows[0]["COUNT(*)"] != 0; + const [rows] = (await db.query("SELECT * FROM users WHERE id = ?", [id])) as [RowDataPacket[], any]; + if (rows.length === 0) { + return await createUser(id); + } + return rows[0] as User; } catch (e: any) { - console.error("Database query failed (isBanned)"); + console.error("Database query failed (getUser)"); console.error(e); throw e; } } -export async function getBannedUsers(): Promise { +async function createUser(id: string): Promise { + const user = { + ...emptyUser, + id + }; + await saveUser(user); + return user; +} + +export async function saveUser(user: User) { try { - const [rows] = (await db.query("SELECT userId FROM bans")) as [RowDataPacket[], any]; - return rows.map((row) => row.userId); + await db.query("INSERT INTO users (id, login, displayName, credit, banned)" + + " VALUES (:id, :login, :displayName, :credit, :banned)" + + " ON DUPLICATE KEY UPDATE login=:login, displayName=:displayName, credit=:credit, banned=:banned", + {...user}); } catch (e: any) { - console.error("Database query failed (getBannedUsers)"); + console.error("Database query failed (saveUser)"); console.error(e); throw e; } diff --git a/ebs/src/util/middleware.ts b/ebs/src/util/middleware.ts index df73d32..11efa83 100644 --- a/ebs/src/util/middleware.ts +++ b/ebs/src/util/middleware.ts @@ -2,8 +2,10 @@ import { NextFunction, Request, Response } from "express"; import { parseJWT, verifyJWT } from "./jwt"; import { AuthorizationPayload } from "../types"; import { sendToLogger } from "./logger"; +import { User } from "../modules/transactions/user"; +import { getUser } from "./db"; -export function publicApiAuth(req: Request, res: Response, next: NextFunction) { +export async function publicApiAuth(req: Request, res: Response, next: NextFunction) { const auth = req.header("Authorization"); if (!auth || !auth.startsWith("Bearer ")) { @@ -17,9 +19,9 @@ export function publicApiAuth(req: Request, res: Response, next: NextFunction) { return; } - req.twitchAuthorization = parseJWT(token) as AuthorizationPayload; + const twitchAuthorization = parseJWT(token) as AuthorizationPayload; - if (!req.twitchAuthorization.user_id) { + if (!twitchAuthorization.user_id) { sendToLogger({ transactionToken: null, userIdInsecure: null, @@ -27,7 +29,7 @@ export function publicApiAuth(req: Request, res: Response, next: NextFunction) { fields: [ { header: "Missing user ID in JWT", - content: req.twitchAuthorization, + content: twitchAuthorization, }, ], }).then(); @@ -35,6 +37,13 @@ export function publicApiAuth(req: Request, res: Response, next: NextFunction) { return; } + req.user = await getUser(twitchAuthorization.user_id); + + if (req.user.banned) { + res.status(403).send("You are banned from using this extension"); + return; + } + next(); } @@ -51,7 +60,7 @@ export function privateApiAuth(req: Request, res: Response, next: NextFunction) declare global { namespace Express { export interface Request { - twitchAuthorization?: AuthorizationPayload; + user: User; } } } diff --git a/frontend/www/src/modules/auth.ts b/frontend/www/src/modules/auth.ts index 658689a..386aa73 100644 --- a/frontend/www/src/modules/auth.ts +++ b/frontend/www/src/modules/auth.ts @@ -3,16 +3,43 @@ import { ebsFetch } from "../util/ebs"; import { hideProcessingModal, openModal, showErrorModal, showSuccessModal, transactionToken } from "./modal"; import { logToDiscord } from "../util/logger"; import { renderRedeemButtons } from "./redeems"; +import { refreshConfig, setConfig } from "../util/config"; const $loginPopup = document.getElementById("onboarding")!; const $loginButton = document.getElementById("twitch-login")!; document.addEventListener("DOMContentLoaded", () => ($loginButton.onclick = Twitch.ext.actions.requestIdShare)); +let _banned = false; +export function getBanned() { + return _banned; +} + +export async function setBanned(banned: boolean) { + if (_banned === banned) return; + + _banned = banned; + if (banned) { + setConfig({ version: -1, message: "You have been banned from using this extension." }); + renderRedeemButtons().then(); + } else { + await refreshConfig(); + renderRedeemButtons().then(); + } +} + Twitch.ext.onAuthorized(() => { $loginPopup.style.display = Twitch.ext.viewer.id ? "none" : ""; if (Twitch.ext.viewer.id) { - renderRedeemButtons().then(); + ebsFetch("public/authorized", { + method: "POST", + body: JSON.stringify({ userId: Twitch.ext.viewer.id }), + }).then((res) => { + if (res.status === 403) { + setBanned(true); + } + renderRedeemButtons().then(); + }); } }); @@ -108,10 +135,10 @@ Twitch.ext.bits.onTransactionCancelled(async () => { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ "token": transactionToken }), + body: JSON.stringify({ token: transactionToken }), }); } - + hideProcessingModal(); showErrorModal("Transaction cancelled.", `Transaction ID: ${transactionToken}`); }); diff --git a/frontend/www/src/modules/pubsub.ts b/frontend/www/src/modules/pubsub.ts index f7cd3ee..06530b4 100644 --- a/frontend/www/src/modules/pubsub.ts +++ b/frontend/www/src/modules/pubsub.ts @@ -1,19 +1,28 @@ -import { Config, PubSubMessage } from "common/types"; -import { postProcessConfig, setConfig } from "../util/config"; +import { BannedData, Config, PubSubMessage } from "common/types"; +import { setConfig } from "../util/config"; import { renderRedeemButtons } from "./redeems"; import { strToU8, decompressSync, strFromU8 } from "fflate"; +import { getBanned, setBanned } from "./auth"; Twitch.ext.listen("global", async (_t, _c, message) => { - const pubSubMessage = JSON.parse(message) as PubSubMessage; + const fullMessage = JSON.parse(message) as PubSubMessage; - console.log(pubSubMessage); + console.log(fullMessage); - switch (pubSubMessage.type) { + switch (fullMessage.type) { case "config_refreshed": - const config = JSON.parse(strFromU8(decompressSync(strToU8(pubSubMessage.data, true)))) as Config; - // console.log(config); - await setConfig(postProcessConfig(config)); - await renderRedeemButtons(); + const config = JSON.parse(strFromU8(decompressSync(strToU8(fullMessage.data, true)))) as Config; + setConfig(config); + if (!getBanned()) { + await renderRedeemButtons(); + } + break; + case "banned": + const data = JSON.parse(fullMessage.data) as BannedData; + const bannedId = data.id; + if (bannedId === Twitch.ext.viewer.id) { + setBanned(data.banned); + } break; } }); diff --git a/frontend/www/src/util/config.ts b/frontend/www/src/util/config.ts index cbdaacc..220f070 100644 --- a/frontend/www/src/util/config.ts +++ b/frontend/www/src/util/config.ts @@ -3,36 +3,29 @@ import { Config } from "common/types"; let config: Config; -export function postProcessConfig(config: Config): Config { - if (config.banned && config.banned.includes(Twitch.ext.viewer.id!)) { - return { - version: -1, - redeems: {}, - enums: {}, - banned: [Twitch.ext.viewer.id!], - message: "You are banned from using this extension", - } satisfies Config; - } - - return config; -} +const emptyConfig: Config = { + version: -1, + redeems: {}, + enums: {}, +}; async function fetchConfig() { const response = await ebsFetch("/public/config"); if (!response.ok) { return { - version: -1, - redeems: {}, - enums: {}, - banned: [], + ...emptyConfig, message: `An error occurred while fetching the config\n${response.status} ${response.statusText} - ${await response.text()}`, } satisfies Config; } const config: Config = await response.json(); - return postProcessConfig(config); + return config; +} + +export async function refreshConfig() { + config = await fetchConfig(); } export async function getConfig(): Promise { @@ -43,6 +36,6 @@ export async function getConfig(): Promise { return config; } -export async function setConfig(newConfig: Config) { +export function setConfig(newConfig: Config) { config = newConfig; } From 7ca0c1d4acea6b8b8816334196875462ea234454 Mon Sep 17 00:00:00 2001 From: Govorunb Date: Thu, 20 Jun 2024 09:14:40 +1000 Subject: [PATCH 2/9] uncringe db schema part 2 --- common/types.ts | 28 +++ ebs/src/modules/game/connection.ts | 13 +- ebs/src/modules/transactions/index.ts | 310 +++++++++++--------------- ebs/src/modules/transactions/order.ts | 137 ++++++++++++ ebs/src/modules/transactions/user.ts | 14 +- ebs/src/util/db.ts | 151 ++++++------- ebs/src/util/middleware.ts | 6 +- logger/src/index.ts | 19 +- logger/src/util/db.ts | 60 +++-- logger/src/util/discord.ts | 3 + 10 files changed, 422 insertions(+), 319 deletions(-) create mode 100644 ebs/src/modules/transactions/order.ts diff --git a/common/types.ts b/common/types.ts index 339ac70..efe6e43 100644 --- a/common/types.ts +++ b/common/types.ts @@ -111,3 +111,31 @@ export type LogMessage = { important: boolean; fields: { header: string; content: any }[]; }; + +export type User = { + id: string, + login?: string, + displayName?: string, + credit: number, + banned: boolean, +} + +export enum OrderState { + Rejected = -1, + Prepurchase, + Cancelled, + Paid, // waiting for game + Failed, // game failed + Succeeded, +} + +export type Order = { + id: string, + userId: string, + state: OrderState, + cart?: Cart, + receipt?: string, + result?: string, + createdAt: number, + updatedAt: number, +} \ No newline at end of file diff --git a/ebs/src/modules/game/connection.ts b/ebs/src/modules/game/connection.ts index 1679639..5a3c28a 100644 --- a/ebs/src/modules/game/connection.ts +++ b/ebs/src/modules/game/connection.ts @@ -3,7 +3,7 @@ import { ResultMessage, GameMessage } from "./messages.game"; import * as ServerWS from "ws"; import { v4 as uuid } from "uuid"; import { CommandInvocationSource, RedeemMessage, ServerMessage } from "./messages.server"; -import { Cart, Redeem } from "common/types"; +import { Order, Redeem } from "common/types"; import { setIngame } from "../config"; const VERSION = "0.1.0"; @@ -129,23 +129,18 @@ export class GameConnection { timestamp: Date.now() } } - public redeem(redeem: Redeem, cart: Cart, user: TwitchUser, transactionId: string) : Promise { + public redeem(redeem: Redeem, order: Order, user: TwitchUser) : Promise { return Promise.race([ new Promise((_, reject) => setTimeout(() => reject(`Timed out waiting for result. The redeem may still go through later, contact Alexejhero if it doesn't.`), GameConnection.resultWaitTimeout)), new Promise((resolve, reject) => { - if (!transactionId) { - reject(`Tried to redeem without transaction ID`); - return; - } - const msg: RedeemMessage = { ...this.makeMessage(MessageType.Redeem), - guid: transactionId, + guid: order.id, source: CommandInvocationSource.Swarm, command: redeem.id, title: redeem.title, announce: redeem.announce ?? true, - args: cart.args, + args: order.cart!.args, user } as RedeemMessage; if (this.outstandingRedeems.has(msg.guid)) { diff --git a/ebs/src/modules/transactions/index.ts b/ebs/src/modules/transactions/index.ts index db861d1..8132a7f 100644 --- a/ebs/src/modules/transactions/index.ts +++ b/ebs/src/modules/transactions/index.ts @@ -1,15 +1,16 @@ -import { Cart, Config, LogMessage, Transaction } from "common/types"; +import { Cart, LogMessage, Order, Transaction } from "common/types"; import { app } from "../.."; import { parseJWT, verifyJWT } from "../../util/jwt"; import { BitsTransactionPayload } from "../../types"; import { getConfig } from "../config"; -import { addFulfilledTransaction, deletePrepurchase, getPrepurchase, isReceiptUsed, registerPrepurchase } from "../../util/db"; +import { createOrder, getOrder, saveOrder } from "../../util/db"; import { sendToLogger } from "../../util/logger"; import { connection } from "../game"; import { TwitchUser } from "../game/messages"; import { asyncCatch } from "../../util/middleware"; import { sendShock } from "../../util/pishock"; import { getTwitchUser } from "../twitch"; +import { validatePrepurchase } from "./order"; require('./user'); @@ -17,16 +18,11 @@ app.post( "/public/prepurchase", asyncCatch(async (req, res) => { const cart = req.body as Cart; - const idCart = { ...cart, userId: req.user.id }; - - if (!connection.isConnected()) { - res.status(502).send("Game connection is not available"); - return; - } - + const userId = req.user.id; + const logContext: LogMessage = { transactionToken: null, - userIdInsecure: idCart.userId, + userIdInsecure: userId, important: false, fields: [ { @@ -37,55 +33,40 @@ app.post( }; const logMessage = logContext.fields[0]; - const config = await getConfig(); - if (cart.version != config.version) { - logMessage.header = "Invalid config version"; - logMessage.content = `Received: ${cart.version}\nExpected: ${config.version}`; - sendToLogger(logContext).then(); - res.status(409).send(`Invalid config version (${cart.version}/${config.version})`); - return; - } - - const redeem = config.redeems?.[cart.id]; - if (!redeem || redeem.sku != cart.sku || redeem.disabled || redeem.hidden) { - logMessage.header = "Invalid redeem"; - logMessage.content = `Received: ${JSON.stringify(cart)}\nRedeem in config: ${JSON.stringify(redeem)}`; - sendToLogger(logContext).then(); - res.status(409).send(`Invalid redeem`); - return; - } - - const valError = validateArgs(config, cart, logContext); - if (valError) { - logMessage.header = "Arg validation failed"; - logMessage.content = { - error: valError, - redeem: cart.id, - expected: redeem.args, - provided: cart.args, - }; - sendToLogger(logContext).then(); - res.status(409).send("Invalid arguments"); + if (!connection.isConnected()) { + res.status(502).send("Game connection is not available"); return; } - - let token: string; + let order: Order; try { - token = await registerPrepurchase(idCart); + order = await createOrder(userId, { cart, state: -1 }); // OrderState.Rejected } catch (e: any) { logContext.important = true; logMessage.header = "Failed to register prepurchase"; - logMessage.content = { cart: idCart, error: e }; + logMessage.content = { cart, userId, error: e }; sendToLogger(logContext).then(); - res.status(500).send("Failed to register prepurchase"); - return; + throw e; } - + logMessage.header = "Created prepurchase"; - logMessage.content = { cart: idCart }; + logMessage.content = { order }; sendToLogger(logContext).then(); - res.status(200).send(token); + let validationError: string | null; + try { + validationError = await validatePrepurchase(order); + } catch (e: any) { + res.status(500).send("Failed to register prepurchase"); + return; + } + if (typeof validationError === "string") { + res.status(409).send(validationError); + return; + } + + order.state = 0;// OrderState.Prepurchase + await saveOrder(order); + res.status(200).send(order.id); }) ); @@ -125,7 +106,35 @@ app.post( const payload = parseJWT(transaction.receipt) as BitsTransactionPayload; - if (await isReceiptUsed(payload.data.transactionId)) { + if (!payload.data.transactionId) { + logMessage.header = "Missing transaction ID"; + logMessage.content = transaction; + sendToLogger(logContext).then(); + res.status(400).send("Missing transaction ID"); + return; + } + let order: Order | null; + try { + order = await getOrder(transaction.token); + } catch (e: any) { + logContext.important = true; + logMessage.header = "Failed to get order"; + logMessage.content = { + transaction: transaction, + error: e, + }; + sendToLogger(logContext).then(); + res.status(500).send("Failed to get transaction"); + return; + } + if (!order) { + logMessage.header = "Transaction not found"; + logMessage.content = transaction; + sendToLogger(logContext).then(); + res.status(404).send("Transaction not found"); + return; + } + if (order.state > 0) { // OrderState.Prepurchase logMessage.header = "Transaction already processed"; logMessage.content = transaction; sendToLogger(logContext).then(); @@ -133,9 +142,7 @@ app.post( return; } - const cart = await getPrepurchase(transaction.token); - - if (!cart) { + if (!order.cart) { logMessage.header = "Invalid transaction token"; logMessage.content = transaction; sendToLogger(logContext).then(); @@ -143,42 +150,44 @@ app.post( return; } - await addFulfilledTransaction(transaction.receipt, transaction.token, req.user.id!); + order.state = 2; // OrderState.Paid + order.receipt = transaction.receipt; + await saveOrder(order); - if (cart.userId != req.user.id) { + if (order.userId != req.user.id) { + // paying for somebody else, how generous logContext.important = true; logMessage.header = "Mismatched user ID"; logMessage.content = { user: req.user, - cart, + order, transaction, }; sendToLogger(logContext).then(); } const currentConfig = await getConfig(); - if (cart.version != currentConfig.version) { + if (order.cart.version != currentConfig.version) { logContext.important = true; logMessage.header = "Mismatched config version"; logMessage.content = { config: currentConfig.version, - cart: cart, + order, transaction: transaction, }; sendToLogger(logContext).then(); } console.log(transaction); - console.log(cart); + console.log(order.cart); - const redeem = currentConfig.redeems?.[cart.id]; + const redeem = currentConfig.redeems?.[order.cart.id]; if (!redeem) { logContext.important = true; logMessage.header = "Redeem not found"; logMessage.content = { - config: currentConfig.version, - cart: cart, - transaction: transaction, + configVersion: currentConfig.version, + order, }; sendToLogger(logContext).then(); res.status(500).send("Redeem could not be found"); @@ -187,30 +196,33 @@ app.post( let userInfo: TwitchUser | null; try { - userInfo = await getTwitchUser(cart.userId); - } catch { + userInfo = await getTwitchUser(order.userId); + } catch (error) { userInfo = null; + console.log(`Error while trying to get Twitch user info: ${error}`); } if (!userInfo) { logContext.important = true; logMessage.header = "Could not get Twitch user info"; logMessage.content = { - config: currentConfig.version, - cart: cart, - transaction: transaction, - error: userInfo, + configVersion: currentConfig.version, + order, }; sendToLogger(logContext).then(); // very much not ideal but they've already paid... so... userInfo = { - id: cart.userId, - login: cart.userId, - displayName: cart.userId, + id: order.userId, + login: order.userId, + displayName: order.userId, }; } try { if (redeem.id == "redeem_pishock") { const success = await sendShock(50, 100); + order.state = success + ? 4 // OrderState.Succeeded + : 3; // OrderState.Failed + await saveOrder(order); if (success) { res.status(200).send("Your transaction was successful!"); } else { @@ -219,7 +231,12 @@ app.post( return; } - const resMsg = await connection.redeem(redeem, cart, userInfo, transaction.token); + const resMsg = await connection.redeem(redeem, order, userInfo); + order.state = resMsg.success + ? 4 // OrderState.Succeeded + : 3; // OrderState.Failed + order.result = resMsg.message; + await saveOrder(order); if (resMsg?.success) { console.log(`[${resMsg.guid}] Redeem succeeded: ${JSON.stringify(resMsg)}`); let msg = "Your transaction was successful! Your redeem will appear on stream soon."; @@ -239,9 +256,8 @@ app.post( logMessage.header = "Failed to send redeem"; logMessage.content = { config: currentConfig.version, - cart: cart, - transaction: transaction, - error: error, + order, + error, }; sendToLogger(logContext).then(); res.status(500).send(`Failed to process redeem - ${error}`); @@ -252,115 +268,53 @@ app.post( app.post( "/public/transaction/cancel", asyncCatch(async (req, res) => { - const token = req.body.token as string; + const guid = req.body.token as string; + const logContext: LogMessage = { + transactionToken: guid, + userIdInsecure: req.user.id, + important: true, + fields: [ + { + header: "", + content: "", + } + ] + }; + const logMessage = logContext.fields[0]; - // remove transaction from db try { - await deletePrepurchase(token); + const order = await getOrder(guid); + + if (!order) { + res.status(404).send("Transaction not found"); + return; + } + + if (order.userId != req.user.id) { + logMessage.header = "Unauthorized transaction cancel"; + logMessage.content = { + order, + user: req.user, + }; + sendToLogger(logContext); + res.status(403).send("This transaction doesn't belong to you"); + return; + } + if (order.state > 0) { // OrderState.Prepurchase + res.status(409).send("Cannot cancel this transaction"); + return; + } + + order.state = 1; // OrderState.Cancelled + await saveOrder(order); res.sendStatus(200); } catch (error) { - sendToLogger({ - transactionToken: token, - userIdInsecure: req.user.id, - important: false, - fields: [ - { - header: "Error deleting transaction", - content: { - error: error, - }, - }, - ], - }).then(); + logMessage.header = "Failed to cancel order"; + logMessage.content = error; + sendToLogger(logContext).then(); - res.sendStatus(404); + res.sendStatus(500); } }) ); - -function validateArgs(config: Config, cart: Cart, logContext: LogMessage): string | undefined { - const redeem = config.redeems![cart.id]; - - for (const arg of redeem.args) { - const value = cart.args[arg.name]; - if (!value) { - if (!arg.required) continue; - - // LiteralTypes.Boolean - if (arg.type === 3) { - // HTML form conventions - false is not transmitted, true is "on" (to save 2 bytes i'm guessing) - continue; - } - - return `Missing required argument ${arg.name}`; - } - let parsed: number; - switch (arg.type) { - // esbuild dies if you use enums - // so we have to use their pure values instead - case 0: // LiteralTypes.String - if (typeof value !== "string") { - return `Argument ${arg.name} not a string`; - } - const minLength = arg.minLength ?? 0; - const maxLength = arg.maxLength ?? 255; - if (value.length < minLength || value.length > maxLength) { - return `Text length out of range for ${arg.name}`; - } - break; - case 1: // LiteralTypes.Integer - case 2: // LiteralTypes.Float - parsed = parseInt(value); - if (Number.isNaN(parsed)) { - return `Argument ${arg.name} is not a number`; - } - // LiteralTypes.Integer - if (arg.type === 1 && parseFloat(value) != parsed) { - return `Argument ${arg.name} is not an integer`; - } - if ((arg.min !== undefined && parsed < arg.min) || (arg.max !== undefined && parsed > arg.max)) { - return `Number ${arg.name} out of range`; - } - break; - case 3: // LiteralTypes.Boolean - if (typeof value !== "boolean" && value !== "true" && value !== "false" && value !== "on") { - return `Argument ${arg.name} not a boolean`; - } - if (value === "on") { - cart.args[arg.name] = true; - } - break; - case 4: // LiteralTypes.Vector - if (!Array.isArray(value) || value.length < 3) { - return `Vector3 ${arg.name} not a 3-elem array`; - } - // workaround for #49 - const lastThree = value.slice(value.length - 3); - for (const v of lastThree) { - parsed = parseFloat(v); - if (Number.isNaN(parsed)) { - return `Vector3 ${arg.name} components not all floats`; - } - } - cart!.args[arg.name] = lastThree; - break; - default: - const argEnum = config.enums?.[arg.type]; - if (!argEnum) { - return `No such enum ${arg.type}`; - } - parsed = parseInt(value); - if (Number.isNaN(parsed) || parsed != parseFloat(value)) { - return `Enum value ${value} (for enum ${arg.type}) not an integer`; - } - if (parsed < 0 || parsed >= argEnum.length) { - return `Enum value ${value} (for enum ${arg.type}) out of range`; - } - if (argEnum[parsed].startsWith("[DISABLED]")) { - return `Enum value ${value} (for enum ${arg.type}) is disabled`; - } - break; - } - } -} diff --git a/ebs/src/modules/transactions/order.ts b/ebs/src/modules/transactions/order.ts new file mode 100644 index 0000000..a64f1a1 --- /dev/null +++ b/ebs/src/modules/transactions/order.ts @@ -0,0 +1,137 @@ +import { Cart, Config, LogMessage, Order } from "common/types"; +import { sendToLogger } from "../../util/logger"; +import { getConfig } from "../config"; + +export async function validatePrepurchase(order: Order) : Promise { + const logContext: LogMessage = { + transactionToken: null, + userIdInsecure: order.userId, + important: false, + fields: [ + { + header: "", + content: "", + }, + ], + }; + const logMessage = logContext.fields[0]; + + const cart = order.cart!; + + const config = await getConfig(); + if (cart.version != config.version) { + logMessage.header = "Invalid config version"; + logMessage.content = `Received: ${cart.version}\nExpected: ${config.version}`; + await sendToLogger(logContext); + return "Invalid config version"; + } + + const redeem = config.redeems?.[cart.id]; + if (!redeem || redeem.sku != cart.sku || redeem.disabled || redeem.hidden) { + logMessage.header = "Invalid redeem"; + logMessage.content = `Received: ${JSON.stringify(cart)}\nRedeem in config: ${JSON.stringify(redeem)}`; + await sendToLogger(logContext); + return "Invalid redeem"; + } + + const valError = validateArgs(config, cart, logContext); + if (valError) { + logMessage.header = "Arg validation failed"; + logMessage.content = { + error: valError, + redeem: cart.id, + expected: redeem.args, + provided: cart.args, + }; + await sendToLogger(logContext); + return "Invalid arguments"; + } + + return null; +} + +function validateArgs(config: Config, cart: Cart, logContext: LogMessage): string | undefined { + const redeem = config.redeems![cart.id]; + + for (const arg of redeem.args) { + const value = cart.args[arg.name]; + if (!value) { + if (!arg.required) continue; + + // LiteralTypes.Boolean + if (arg.type === 3) { + // HTML form conventions - false is not transmitted, true is "on" (to save 2 bytes i'm guessing) + continue; + } + + return `Missing required argument ${arg.name}`; + } + let parsed: number; + switch (arg.type) { + // esbuild dies if you use enums + // so we have to use their pure values instead + case 0: // LiteralTypes.String + if (typeof value !== "string") { + return `Argument ${arg.name} not a string`; + } + const minLength = arg.minLength ?? 0; + const maxLength = arg.maxLength ?? 255; + if (value.length < minLength || value.length > maxLength) { + return `Text length out of range for ${arg.name}`; + } + break; + case 1: // LiteralTypes.Integer + case 2: // LiteralTypes.Float + parsed = parseInt(value); + if (Number.isNaN(parsed)) { + return `Argument ${arg.name} is not a number`; + } + // LiteralTypes.Integer + if (arg.type === 1 && parseFloat(value) != parsed) { + return `Argument ${arg.name} is not an integer`; + } + if ((arg.min !== undefined && parsed < arg.min) || (arg.max !== undefined && parsed > arg.max)) { + return `Number ${arg.name} out of range`; + } + break; + case 3: // LiteralTypes.Boolean + if (typeof value !== "boolean" && value !== "true" && value !== "false" && value !== "on") { + return `Argument ${arg.name} not a boolean`; + } + if (value === "on") { + cart.args[arg.name] = true; + } + break; + case 4: // LiteralTypes.Vector + if (!Array.isArray(value) || value.length < 3) { + return `Vector3 ${arg.name} not a 3-elem array`; + } + // workaround for #49 + const lastThree = value.slice(value.length - 3); + for (const v of lastThree) { + parsed = parseFloat(v); + if (Number.isNaN(parsed)) { + return `Vector3 ${arg.name} components not all floats`; + } + } + cart!.args[arg.name] = lastThree; + break; + default: + const argEnum = config.enums?.[arg.type]; + if (!argEnum) { + return `No such enum ${arg.type}`; + } + parsed = parseInt(value); + if (Number.isNaN(parsed) || parsed != parseFloat(value)) { + return `Enum value ${value} (for enum ${arg.type}) not an integer`; + } + if (parsed < 0 || parsed >= argEnum.length) { + return `Enum value ${value} (for enum ${arg.type}) out of range`; + } + if (argEnum[parsed].startsWith("[DISABLED]")) { + return `Enum value ${value} (for enum ${arg.type}) is disabled`; + } + break; + } + } +} \ No newline at end of file diff --git a/ebs/src/modules/transactions/user.ts b/ebs/src/modules/transactions/user.ts index d72534f..3ff8eb3 100644 --- a/ebs/src/modules/transactions/user.ts +++ b/ebs/src/modules/transactions/user.ts @@ -1,18 +1,10 @@ import { app } from "../.."; -import { getUser, saveUser } from "../../util/db"; +import { getOrAddUser, saveUser } from "../../util/db"; import { asyncCatch } from "../../util/middleware"; import { sendPubSubMessage } from "../../util/pubsub"; -export type User = { - id: string, - login?: string, - displayName?: string, - credit: number, - banned: boolean, -} - export async function setUserBanned(id: string, banned: boolean) { - const user = await getUser(id); + const user = await getOrAddUser(id); user.banned = banned; await saveUser(user); await sendPubSubMessage({ @@ -40,7 +32,7 @@ app.delete("/private/ban/:id", asyncCatch(async (req, res) => { app.post("/private/credit/:id", asyncCatch(async (req, res) => { const id = req.params["id"]; const amount = req.body.amount as number; - const user = await getUser(id); + const user = await getOrAddUser(id); user.credit += amount; await saveUser(user); res.sendStatus(200); diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index 5b7ae76..c9a1220 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -1,16 +1,12 @@ import { RowDataPacket } from "mysql2"; import mysql from "mysql2/promise"; -import { IdentifiableCart } from "common/types"; import { v4 as uuid } from "uuid"; -import { User } from "../modules/transactions/user"; +import { Order } from "common/types"; +import { User } from "common/types"; export let db: mysql.Connection; export async function initDb() { - if (!process.env.MYSQL_HOST) { - console.warn("No MYSQL_HOST specified (assuming local testing/development), skipping database setup"); - return; - } while (!db) { try { db = await mysql.createConnection({ @@ -29,7 +25,7 @@ export async function initDb() { await setupDb(); } -export async function setupDb() { +async function setupDb() { await db.query(` CREATE TABLE IF NOT EXISTS users ( id VARCHAR(255) PRIMARY KEY, @@ -40,96 +36,68 @@ export async function setupDb() { ); `); await db.query(` - CREATE TABLE IF NOT EXISTS transactions ( - receipt VARCHAR(1024) PRIMARY KEY, - token VARCHAR(255) NOT NULL, - userId VARCHAR(255) NOT NULL - ); - `); - - await db.query(` - CREATE TABLE IF NOT EXISTS prepurchases ( - token VARCHAR(255) PRIMARY KEY, - cart JSON NOT NULL, - userId VARCHAR(255) NOT NULL - ); - `); - - await db.query(` - CREATE TABLE IF NOT EXISTS logs ( - id INT PRIMARY KEY AUTO_INCREMENT, - userId VARCHAR(255), - transactionToken VARCHAR(255), - data TEXT NOT NULL, - fromBackend BOOLEAN NOT NULL, - timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + CREATE TABLE IF NOT EXISTS orders ( + id VARCHAR(36) PRIMARY KEY, + userId VARCHAR(255) NOT NULL, + state INT NOT NULL DEFAULT 0, + cart JSON, + receipt VARCHAR(1024), + result TEXT, + createdAt BIGINT, + updatedAt BIGINT ); `); } -export async function isReceiptUsed(receipt: string): Promise { - try { - const [rows] = (await db.query("SELECT COUNT(*) FROM transactions WHERE receipt = ?", [receipt])) as [RowDataPacket[], any]; - return rows[0]["COUNT(*)"] != 0; - } catch (e: any) { - console.error("Database query failed (isReceiptUsed)"); - console.error(e); - throw e; - } -} - -export async function addFulfilledTransaction(receipt: string, token: string, userId: string) { - try { - await db.query("INSERT INTO transactions (receipt, token, userId) VALUES (?, ?, ?)", [receipt, token, userId]); - await db.query("DELETE FROM prepurchases WHERE token = ?", [token]); - } catch (e: any) { - console.error("Database query failed (addFulfilledTransaction)"); - console.error(e); - throw e; - } -} - -export async function registerPrepurchase(cart: IdentifiableCart): Promise { +export async function getOrder(guid: string) { try { - const token = uuid(); - await db.query("INSERT INTO prepurchases (token, cart, userId) VALUES (?, ?, ?)", [token, JSON.stringify(cart), cart.userId]); - return token; + const [rows] = (await db.query("SELECT * FROM orders WHERE id = ?", [guid])) as [RowDataPacket[], any]; + if (!rows.length) { + return null; + } + return rows[0] as Order; } catch (e: any) { - console.error("Database query failed (registerPrepurchase)"); + console.error("Database query failed (getOrder)"); console.error(e); throw e; } } -export async function getPrepurchase(token: string): Promise { +export async function createOrder(userId: string, initialState?: Omit, "id" | "userId" | "createdAt" | "updatedAt">) { + const order: Order = { + state: 0, + ...initialState, + id: uuid(), + userId, + createdAt: Date.now(), + updatedAt: Date.now(), + }; try { - const [rows] = (await db.query("SELECT cart FROM prepurchases WHERE token = ?", [token])) as [RowDataPacket[], any]; - if (rows.length === 0) return undefined; - return rows[0].cart as IdentifiableCart; + await db.query(` + INSERT INTO orders (id, userId, state, cart, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?)`, + [order.id, order.userId, order.state, JSON.stringify(order.cart), order.createdAt, order.updatedAt]); + return order; } catch (e: any) { - console.error("Database query failed (isPrepurchaseValid)"); + console.error("Database query failed (createOrder)"); console.error(e); throw e; } } -export async function deletePrepurchase(token: string) { - try { - await db.query("DELETE FROM prepurchases WHERE token = ?", [token]); - } catch (e: any) { - console.error("Database query failed (deletePrepurchase)"); - console.error(e); - throw e; - } +export async function saveOrder(order: Order) { + order.updatedAt = Date.now(); + await db.query( + ` + UPDATE orders + SET state = ?, cart = ?, receipt = ?, result = ?, updatedAt = ? + WHERE id = ? + `, + [order.state, JSON.stringify(order.cart), order.receipt, order.receipt, order.updatedAt, order.id] + ); } -const emptyUser: User = { - id: "", - credit: 0, - banned: false -}; - -export async function getUser(id: string): Promise { +export async function getOrAddUser(id: string): Promise { try { const [rows] = (await db.query("SELECT * FROM users WHERE id = ?", [id])) as [RowDataPacket[], any]; if (rows.length === 0) { @@ -145,19 +113,34 @@ export async function getUser(id: string): Promise { async function createUser(id: string): Promise { const user = { - ...emptyUser, - id + id, + credit: 0, + banned: false, }; - await saveUser(user); + try { + await db.query( + ` + INSERT INTO users (id, login, displayName, credit, banned) + VALUES (:id, :login, :displayName, :credit, :banned)`, + user + ); + } catch (e: any) { + console.error("Database query failed (createUser)"); + console.error(e); + throw e; + } return user; } export async function saveUser(user: User) { try { - await db.query("INSERT INTO users (id, login, displayName, credit, banned)" - + " VALUES (:id, :login, :displayName, :credit, :banned)" - + " ON DUPLICATE KEY UPDATE login=:login, displayName=:displayName, credit=:credit, banned=:banned", - {...user}); + await db.query( + ` + UPDATE users + SET login = :login, displayName = :displayName, credit = :credit, banned = :banned + WHERE id = :id`, + { ...user } + ); } catch (e: any) { console.error("Database query failed (saveUser)"); console.error(e); diff --git a/ebs/src/util/middleware.ts b/ebs/src/util/middleware.ts index 11efa83..547c3cb 100644 --- a/ebs/src/util/middleware.ts +++ b/ebs/src/util/middleware.ts @@ -2,8 +2,8 @@ import { NextFunction, Request, Response } from "express"; import { parseJWT, verifyJWT } from "./jwt"; import { AuthorizationPayload } from "../types"; import { sendToLogger } from "./logger"; -import { User } from "../modules/transactions/user"; -import { getUser } from "./db"; +import { User } from "common/types"; +import { getOrAddUser } from "./db"; export async function publicApiAuth(req: Request, res: Response, next: NextFunction) { const auth = req.header("Authorization"); @@ -37,7 +37,7 @@ export async function publicApiAuth(req: Request, res: Response, next: NextFunct return; } - req.user = await getUser(twitchAuthorization.user_id); + req.user = await getOrAddUser(twitchAuthorization.user_id); if (req.user.banned) { res.status(403).send("You are banned from using this extension"); diff --git a/logger/src/index.ts b/logger/src/index.ts index 262e5a5..37bd0e3 100644 --- a/logger/src/index.ts +++ b/logger/src/index.ts @@ -2,7 +2,7 @@ import { config as dotenv } from "dotenv"; import cors from "cors"; import express from "express"; import bodyParser from "body-parser"; -import mysql from "mysql2/promise"; +import { initDb } from "./util/db"; dotenv(); @@ -16,23 +16,8 @@ app.get("/", (_, res) => { res.send("YOU ARE TRESPASSING ON PRIVATE PROPERTY YOU HAVE 5 SECONDS TO GET OUT OR I WILL CALL THE POLICE"); }); -export let db: mysql.Connection; - async function main() { - while (true) { - try { - db = await mysql.createConnection({ - host: process.env.MYSQL_HOST, - user: process.env.MYSQL_USER, - password: process.env.MYSQL_PASSWORD, - database: process.env.MYSQL_DATABASE, - }); - break; - } catch { - console.log("Failed to connect to database. Retrying in 5 seconds..."); - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - } + await initDb(); app.listen(port, () => { console.log("Listening on port " + port); diff --git a/logger/src/util/db.ts b/logger/src/util/db.ts index 49bdb7e..946cbb2 100644 --- a/logger/src/util/db.ts +++ b/logger/src/util/db.ts @@ -1,20 +1,51 @@ -import { db } from ".."; import { RowDataPacket } from "mysql2"; -import { LogMessage } from "common/types"; +import { LogMessage, Order } from "common/types"; import { stringify } from "./stringify"; import { logToDiscord } from "./discord"; +import mysql from "mysql2/promise"; + +export let db: mysql.Connection; + +export async function initDb() { + while (!db) { + try { + db = await mysql.createConnection({ + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + namedPlaceholders: true, + }); + } catch { + console.log("Failed to connect to database. Retrying in 5 seconds..."); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } + + await setupDb(); +} + +async function setupDb() { + await db.query(` + CREATE TABLE IF NOT EXISTS logs ( + id INT PRIMARY KEY AUTO_INCREMENT, + userId VARCHAR(255), + transactionToken VARCHAR(255), + data TEXT NOT NULL, + fromBackend BOOLEAN NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); +} export async function canLog(token: string | null): Promise { try { if (!token) return false; - const [rows] = (await db.query("SELECT COUNT(*) FROM prepurchases WHERE token = ?", [token])) as any; - if (rows[0]["COUNT(*)"] != 0) return true; - - const [rows2] = (await db.query("SELECT COUNT(*) FROM transactions WHERE token = ?", [token])) as any; - if (rows2[0]["COUNT(*)"] != 0) return true; + const [rows] = (await db.query("SELECT * FROM orders WHERE id = ?", [token])) as any; + const order = rows[0] as Order | undefined; - return false; + return order?.state === 0; // OrderState.Prepurchase } catch (e: any) { console.error("Database query failed (canLog)"); console.error(e); @@ -24,13 +55,8 @@ export async function canLog(token: string | null): Promise { export async function getUserIdFromTransactionToken(token: string): Promise { try { - try { - const [rows] = (await db.query("SELECT userId FROM prepurchases WHERE token = ?", [token])) as [RowDataPacket[], any]; - return rows[0].userId; - } catch (e: any) { - const [rows] = (await db.query("SELECT userId FROM transactions WHERE token = ?", [token])) as [RowDataPacket[], any]; - return rows[0].userId; - } + const [rows] = (await db.query("SELECT userId FROM orders WHERE id = ?", [token])) as [RowDataPacket[], any]; + return rows[0].userId; } catch (e: any) { console.error("Database query failed (getUserIdFromTransactionToken)"); console.error(e); @@ -40,8 +66,8 @@ export async function getUserIdFromTransactionToken(token: string): Promise { try { - const [rows] = (await db.query("SELECT COUNT(*) FROM bans WHERE userId = ?", [userId])) as [RowDataPacket[], any]; - return rows[0]["COUNT(*)"] != 0; + const [rows] = (await db.query("SELECT banned FROM users WHERE id = ?", [userId])) as [RowDataPacket[], any]; + return rows[0]?.banned; } catch (e: any) { console.error("Database query failed (isBanned)"); console.error(e); diff --git a/logger/src/util/discord.ts b/logger/src/util/discord.ts index 7f54dcb..64c2ea9 100644 --- a/logger/src/util/discord.ts +++ b/logger/src/util/discord.ts @@ -1,6 +1,9 @@ import { Webhook } from "@vermaysha/discord-webhook"; import { LogMessage } from "common/types"; import { stringify } from "./stringify"; +import { config as dotenv } from "dotenv"; + +dotenv(); const hook = new Webhook(process.env.DISCORD_WEBHOOK!); hook.setUsername("Swarm Control"); From 04d942fbc5b2e22715fd55d09f8dc6b61485af50 Mon Sep 17 00:00:00 2001 From: Govorunb Date: Thu, 20 Jun 2024 10:04:41 +1000 Subject: [PATCH 3/9] part 3 --- common/types.ts | 21 ---- ebs/src/index.ts | 2 +- ebs/src/modules/game/connection.ts | 14 ++- .../modules/{transactions => orders}/index.ts | 110 +++++++++--------- ebs/src/modules/orders/order.ts | 20 ++++ .../order.ts => orders/prepurchase.ts} | 0 ebs/src/modules/orders/user.ts | 53 +++++++++ ebs/src/modules/transactions/user.ts | 39 ------- ebs/src/modules/twitch.ts | 6 - ebs/src/util/db.ts | 61 ++++++++-- frontend/www/src/modules/modal.ts | 2 + 11 files changed, 192 insertions(+), 136 deletions(-) rename ebs/src/modules/{transactions => orders}/index.ts (77%) create mode 100644 ebs/src/modules/orders/order.ts rename ebs/src/modules/{transactions/order.ts => orders/prepurchase.ts} (100%) create mode 100644 ebs/src/modules/orders/user.ts delete mode 100644 ebs/src/modules/transactions/user.ts diff --git a/common/types.ts b/common/types.ts index efe6e43..f79ef72 100644 --- a/common/types.ts +++ b/common/types.ts @@ -116,26 +116,5 @@ export type User = { id: string, login?: string, displayName?: string, - credit: number, banned: boolean, } - -export enum OrderState { - Rejected = -1, - Prepurchase, - Cancelled, - Paid, // waiting for game - Failed, // game failed - Succeeded, -} - -export type Order = { - id: string, - userId: string, - state: OrderState, - cart?: Cart, - receipt?: string, - result?: string, - createdAt: number, - updatedAt: number, -} \ No newline at end of file diff --git a/ebs/src/index.ts b/ebs/src/index.ts index 4548877..2179461 100644 --- a/ebs/src/index.ts +++ b/ebs/src/index.ts @@ -28,7 +28,7 @@ async function main() { console.log("Listening on port " + port); require("./modules/config"); - require("./modules/transactions"); + require("./modules/orders"); require("./modules/game"); require("./modules/twitch"); diff --git a/ebs/src/modules/game/connection.ts b/ebs/src/modules/game/connection.ts index 5a3c28a..0ebafcf 100644 --- a/ebs/src/modules/game/connection.ts +++ b/ebs/src/modules/game/connection.ts @@ -131,7 +131,7 @@ export class GameConnection { } public redeem(redeem: Redeem, order: Order, user: TwitchUser) : Promise { return Promise.race([ - new Promise((_, reject) => setTimeout(() => reject(`Timed out waiting for result. The redeem may still go through later, contact Alexejhero if it doesn't.`), GameConnection.resultWaitTimeout)), + new Promise((_, reject) => setTimeout(() => reject(`Timed out waiting for result. The redeem may still go through later, contact AlexejheroDev if it doesn't.`), GameConnection.resultWaitTimeout)), new Promise((resolve, reject) => { const msg: RedeemMessage = { ...this.makeMessage(MessageType.Redeem), @@ -185,4 +185,16 @@ export class GameConnection { public getOutstanding() { return Array.from(this.outstandingRedeems.values()); } + + public onResult(guid: string, resolve: (result: ResultMessage) => void) { + const existing = this.resultHandlers.get(guid); + if (existing) { + this.resultHandlers.set(guid, (result: ResultMessage) => { + existing(result); + resolve(result); + }) + } else { + this.resultHandlers.set(guid, resolve); + } + } } diff --git a/ebs/src/modules/transactions/index.ts b/ebs/src/modules/orders/index.ts similarity index 77% rename from ebs/src/modules/transactions/index.ts rename to ebs/src/modules/orders/index.ts index 8132a7f..60513d2 100644 --- a/ebs/src/modules/transactions/index.ts +++ b/ebs/src/modules/orders/index.ts @@ -1,16 +1,17 @@ -import { Cart, LogMessage, Order, Transaction } from "common/types"; +import { Cart, LogMessage, Transaction } from "common/types"; import { app } from "../.."; import { parseJWT, verifyJWT } from "../../util/jwt"; import { BitsTransactionPayload } from "../../types"; import { getConfig } from "../config"; -import { createOrder, getOrder, saveOrder } from "../../util/db"; +import { createOrder, getOrder, saveOrder, updateUserTwitchInfo } from "../../util/db"; import { sendToLogger } from "../../util/logger"; import { connection } from "../game"; import { TwitchUser } from "../game/messages"; import { asyncCatch } from "../../util/middleware"; import { sendShock } from "../../util/pishock"; -import { getTwitchUser } from "../twitch"; -import { validatePrepurchase } from "./order"; +import { validatePrepurchase } from "./prepurchase"; +import { setUserBanned } from "./user"; +import { Order } from "./order"; require('./user'); @@ -39,7 +40,7 @@ app.post( } let order: Order; try { - order = await createOrder(userId, { cart, state: -1 }); // OrderState.Rejected + order = await createOrder(userId, { cart }); } catch (e: any) { logContext.important = true; logMessage.header = "Failed to register prepurchase"; @@ -64,7 +65,7 @@ app.post( return; } - order.state = 0;// OrderState.Prepurchase + order.state = "prepurchase"; await saveOrder(order); res.status(200).send(order.id); }) @@ -82,7 +83,7 @@ app.post( fields: [ { header: "", - content: "", + content: transaction, }, ], }; @@ -90,7 +91,6 @@ app.post( if (!transaction.receipt) { logMessage.header = "Missing receipt"; - logMessage.content = transaction; sendToLogger(logContext).then(); res.status(400).send("Missing receipt"); return; @@ -98,8 +98,8 @@ app.post( if (!verifyJWT(transaction.receipt)) { logMessage.header = "Invalid receipt"; - logMessage.content = transaction; sendToLogger(logContext).then(); + setUserBanned(req.user.id, true); res.status(403).send("Invalid receipt."); return; } @@ -108,7 +108,6 @@ app.post( if (!payload.data.transactionId) { logMessage.header = "Missing transaction ID"; - logMessage.content = transaction; sendToLogger(logContext).then(); res.status(400).send("Missing transaction ID"); return; @@ -129,28 +128,25 @@ app.post( } if (!order) { logMessage.header = "Transaction not found"; - logMessage.content = transaction; sendToLogger(logContext).then(); res.status(404).send("Transaction not found"); return; } - if (order.state > 0) { // OrderState.Prepurchase + if (order.state != "prepurchase") { logMessage.header = "Transaction already processed"; - logMessage.content = transaction; sendToLogger(logContext).then(); res.status(409).send("Transaction already processed"); return; } if (!order.cart) { - logMessage.header = "Invalid transaction token"; - logMessage.content = transaction; + logMessage.header = "Invalid transaction"; sendToLogger(logContext).then(); - res.status(404).send("Invalid transaction token"); + res.status(500).send("Invalid transaction"); return; } - order.state = 2; // OrderState.Paid + order.state = "paid"; order.receipt = transaction.receipt; await saveOrder(order); @@ -173,7 +169,7 @@ app.post( logMessage.content = { config: currentConfig.version, order, - transaction: transaction, + transaction, }; sendToLogger(logContext).then(); } @@ -194,47 +190,41 @@ app.post( return; } - let userInfo: TwitchUser | null; - try { - userInfo = await getTwitchUser(order.userId); - } catch (error) { - userInfo = null; - console.log(`Error while trying to get Twitch user info: ${error}`); + let userInfo: TwitchUser = { + id: req.user.id, + login: req.user.login ?? req.user.id, + displayName: req.user.displayName ?? req.user.id, + }; + if (!req.user.login || !req.user.displayName) { + try { + await updateUserTwitchInfo(req.user); + } catch (error) { + logContext.important = true; + logMessage.header = "Could not get Twitch user info"; + logMessage.content = { + configVersion: currentConfig.version, + order, + }; + sendToLogger(logContext).then(); + // very much not ideal but they've already paid... so... + console.log(`Error while trying to get Twitch user info: ${error}`); + } } - if (!userInfo) { - logContext.important = true; - logMessage.header = "Could not get Twitch user info"; - logMessage.content = { - configVersion: currentConfig.version, - order, - }; - sendToLogger(logContext).then(); - // very much not ideal but they've already paid... so... - userInfo = { - id: order.userId, - login: order.userId, - displayName: order.userId, - }; + + if (redeem.id == "redeem_pishock") { + const success = await sendShock(50, 100); + order.state = success ? "succeeded" : "failed"; + await saveOrder(order); + if (success) { + res.status(200).send("Your transaction was successful!"); + } else { + res.status(500).send("Redeem failed"); + } + return; } try { - if (redeem.id == "redeem_pishock") { - const success = await sendShock(50, 100); - order.state = success - ? 4 // OrderState.Succeeded - : 3; // OrderState.Failed - await saveOrder(order); - if (success) { - res.status(200).send("Your transaction was successful!"); - } else { - res.status(500).send("Redeem failed"); - } - return; - } - const resMsg = await connection.redeem(redeem, order, userInfo); - order.state = resMsg.success - ? 4 // OrderState.Succeeded - : 3; // OrderState.Failed + order.state = resMsg.success ? "succeeded" : "failed"; order.result = resMsg.message; await saveOrder(order); if (resMsg?.success) { @@ -260,6 +250,12 @@ app.post( error, }; sendToLogger(logContext).then(); + connection.onResult(order.id, (res) => { + console.log(`Got late result (from re-send?) for ${order.id}`); + order.state = res.success ? "succeeded" : "failed"; + order.result = res.message; + saveOrder(order).then(); + }); res.status(500).send(`Failed to process redeem - ${error}`); } }) @@ -301,12 +297,12 @@ app.post( return; } - if (order.state > 0) { // OrderState.Prepurchase + if (order.state !== "prepurchase") { res.status(409).send("Cannot cancel this transaction"); return; } - order.state = 1; // OrderState.Cancelled + order.state = "cancelled"; await saveOrder(order); res.sendStatus(200); } catch (error) { diff --git a/ebs/src/modules/orders/order.ts b/ebs/src/modules/orders/order.ts new file mode 100644 index 0000000..c24064b --- /dev/null +++ b/ebs/src/modules/orders/order.ts @@ -0,0 +1,20 @@ +import { Cart } from "common/types"; + +export type OrderState += "rejected" +| "prepurchase" +| "cancelled" +| "paid" // waiting for game +| "failed" // game failed/timed out +| "succeeded"; + +export type Order = { + id: string, + userId: string, + state: OrderState, + cart?: Cart, + receipt?: string, + result?: string, + createdAt: number, + updatedAt: number, +} \ No newline at end of file diff --git a/ebs/src/modules/transactions/order.ts b/ebs/src/modules/orders/prepurchase.ts similarity index 100% rename from ebs/src/modules/transactions/order.ts rename to ebs/src/modules/orders/prepurchase.ts diff --git a/ebs/src/modules/orders/user.ts b/ebs/src/modules/orders/user.ts new file mode 100644 index 0000000..094ff02 --- /dev/null +++ b/ebs/src/modules/orders/user.ts @@ -0,0 +1,53 @@ +import { app } from "../.."; +import { getOrAddUser, saveUser, updateUserTwitchInfo } from "../../util/db"; +import { asyncCatch } from "../../util/middleware"; +import { sendPubSubMessage } from "../../util/pubsub"; + +export async function setUserBanned(id: string, banned: boolean) { + const user = await getOrAddUser(id); + user.banned = banned; + await saveUser(user); + await sendPubSubMessage({ + type: "banned", + data: JSON.stringify({ id, banned }), + }); +} + +app.post( + "/public/authorized", + asyncCatch(async (req, res) => { + res.sendStatus(200); + updateUserTwitchInfo(req.user).then().catch(console.error); + }) +); + +app.get("/private/user/:id", asyncCatch(async (req, res) => { + res.json(await getOrAddUser(req.params["id"])); +})); + +app.get( + "/private/user/:id/ban", + asyncCatch(async (req, res) => { + const id = req.params["id"]; + const user = await getOrAddUser(id); + res.send({ banned: user.banned }); + }) +); + +app.post( + "/private/user/:id/ban", + asyncCatch(async (req, res) => { + const id = req.params["id"]; + await setUserBanned(id, true); + res.sendStatus(200); + }) +); + +app.delete( + "/private/user/:id/ban", + asyncCatch(async (req, res) => { + const id = req.params["id"]; + await setUserBanned(id, false); + res.sendStatus(200); + }) +); diff --git a/ebs/src/modules/transactions/user.ts b/ebs/src/modules/transactions/user.ts deleted file mode 100644 index 3ff8eb3..0000000 --- a/ebs/src/modules/transactions/user.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { app } from "../.."; -import { getOrAddUser, saveUser } from "../../util/db"; -import { asyncCatch } from "../../util/middleware"; -import { sendPubSubMessage } from "../../util/pubsub"; - -export async function setUserBanned(id: string, banned: boolean) { - const user = await getOrAddUser(id); - user.banned = banned; - await saveUser(user); - await sendPubSubMessage({ - type: "banned", - data: JSON.stringify({id, banned}), - }); -} - -app.post("/public/authorized", asyncCatch(async (req, res) => { - res.sendStatus(200); -})) - -app.post("/private/ban/:id", asyncCatch(async (req, res) => { - const id = req.params["id"]; - await setUserBanned(id, true); - res.sendStatus(200); -})) - -app.delete("/private/ban/:id", asyncCatch(async (req, res) => { - const id = req.params["id"]; - await setUserBanned(id, false); - res.sendStatus(200); -})) - -app.post("/private/credit/:id", asyncCatch(async (req, res) => { - const id = req.params["id"]; - const amount = req.body.amount as number; - const user = await getOrAddUser(id); - user.credit += amount; - await saveUser(user); - res.sendStatus(200); -})) \ No newline at end of file diff --git a/ebs/src/modules/twitch.ts b/ebs/src/modules/twitch.ts index 1643881..93499a7 100644 --- a/ebs/src/modules/twitch.ts +++ b/ebs/src/modules/twitch.ts @@ -1,12 +1,6 @@ -import { app } from ".."; -import { asyncCatch } from "../util/middleware"; import { getHelixUser } from "../util/twitch"; import { TwitchUser } from "./game/messages"; -app.get("/private/user/:id", asyncCatch(async (req, res) => { - res.json(await getTwitchUser(req.params["id"])); -})); - export async function getTwitchUser(id: string): Promise { const user = await getHelixUser(id); if (!user) { diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index c9a1220..9585eb1 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -1,8 +1,9 @@ import { RowDataPacket } from "mysql2"; import mysql from "mysql2/promise"; import { v4 as uuid } from "uuid"; -import { Order } from "common/types"; +import { Order } from "../modules/orders/order"; import { User } from "common/types"; +import { getTwitchUser } from "../modules/twitch"; export let db: mysql.Connection; @@ -31,7 +32,6 @@ async function setupDb() { id VARCHAR(255) PRIMARY KEY, login VARCHAR(255), displayName VARCHAR(255), - credit INT, banned BOOLEAN ); `); @@ -39,7 +39,7 @@ async function setupDb() { CREATE TABLE IF NOT EXISTS orders ( id VARCHAR(36) PRIMARY KEY, userId VARCHAR(255) NOT NULL, - state INT NOT NULL DEFAULT 0, + state ENUM('rejected', 'prepurchase', 'cancelled', 'paid', 'failed', 'succeeded'), cart JSON, receipt VARCHAR(1024), result TEXT, @@ -65,7 +65,7 @@ export async function getOrder(guid: string) { export async function createOrder(userId: string, initialState?: Omit, "id" | "userId" | "createdAt" | "updatedAt">) { const order: Order = { - state: 0, + state: "rejected", ...initialState, id: uuid(), userId, @@ -98,10 +98,24 @@ export async function saveOrder(order: Order) { } export async function getOrAddUser(id: string): Promise { + try { + let user = await getUser(id); + if (!user) { + user = await createUser(id); + } + return user; + } catch (e: any) { + console.error("Database query failed (getOrAddUser)"); + console.error(e); + throw e; + } +} + +export async function getUser(id: string) : Promise { try { const [rows] = (await db.query("SELECT * FROM users WHERE id = ?", [id])) as [RowDataPacket[], any]; - if (rows.length === 0) { - return await createUser(id); + if (!rows.length) { + return null; } return rows[0] as User; } catch (e: any) { @@ -112,16 +126,15 @@ export async function getOrAddUser(id: string): Promise { } async function createUser(id: string): Promise { - const user = { + const user: User = { id, - credit: 0, banned: false, }; try { await db.query( ` - INSERT INTO users (id, login, displayName, credit, banned) - VALUES (:id, :login, :displayName, :credit, :banned)`, + INSERT INTO users (id, login, displayName, banned) + VALUES (:id, :login, :displayName, :banned)`, user ); } catch (e: any) { @@ -137,7 +150,7 @@ export async function saveUser(user: User) { await db.query( ` UPDATE users - SET login = :login, displayName = :displayName, credit = :credit, banned = :banned + SET login = :login, displayName = :displayName, banned = :banned WHERE id = :id`, { ...user } ); @@ -147,3 +160,29 @@ export async function saveUser(user: User) { throw e; } } + +export async function updateUserTwitchInfo(user: User) { + try { + user = { + ...user, + ...await getTwitchUser(user.id), + }; + } catch (e: any) { + console.error("Twitch API GetUsers call failed (updateUserTwitchInfo)"); + console.error(e); + throw e; + } + try { + await db.query( + ` + UPDATE users + SET login = :login, displayName = :displayName + WHERE id = :id`, + { ...user } + ); + } catch (e: any) { + console.error("Database query failed (updateUserTwitchInfo)"); + console.error(e); + throw e; + } +} diff --git a/frontend/www/src/modules/modal.ts b/frontend/www/src/modules/modal.ts index e806e3d..bacb6df 100644 --- a/frontend/www/src/modules/modal.ts +++ b/frontend/www/src/modules/modal.ts @@ -2,6 +2,7 @@ import { BooleanParam, Cart, EnumParam, LiteralTypes, NumericParam, Parameter, R import { ebsFetch } from "../util/ebs"; import { getConfig } from "../util/config"; import { logToDiscord } from "../util/logger"; +import { setBanned } from "./auth"; document.body.addEventListener("dblclick", (e) => { e.stopPropagation(); @@ -247,6 +248,7 @@ async function prePurchase() { if (!response.ok) { hideProcessingModal(); if (response.status == 403) { + setBanned(true); showErrorModal("You are banned from using this extension.", `${response.status} ${response.statusText} - ${await response.text()}\n`); } else { showErrorModal( From 3eb5668b51371f6f91fc56cd45eb349f5aadf931 Mon Sep 17 00:00:00 2001 From: Govorunb Date: Thu, 20 Jun 2024 10:36:18 +1000 Subject: [PATCH 4/9] schizo over --- ebs/src/modules/orders/user.ts | 43 +++++++++++++++++----------------- ebs/src/util/db.ts | 12 +++++----- ebs/src/util/twitch.ts | 3 +++ 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/ebs/src/modules/orders/user.ts b/ebs/src/modules/orders/user.ts index 094ff02..89c1ebf 100644 --- a/ebs/src/modules/orders/user.ts +++ b/ebs/src/modules/orders/user.ts @@ -1,15 +1,15 @@ +import { User } from "common/types"; import { app } from "../.."; -import { getOrAddUser, saveUser, updateUserTwitchInfo } from "../../util/db"; +import { lookupUser, saveUser, updateUserTwitchInfo } from "../../util/db"; import { asyncCatch } from "../../util/middleware"; import { sendPubSubMessage } from "../../util/pubsub"; -export async function setUserBanned(id: string, banned: boolean) { - const user = await getOrAddUser(id); +export async function setUserBanned(user: User, banned: boolean) { user.banned = banned; await saveUser(user); await sendPubSubMessage({ type: "banned", - data: JSON.stringify({ id, banned }), + data: JSON.stringify({ id: user.id, banned }), }); } @@ -21,33 +21,34 @@ app.post( }) ); -app.get("/private/user/:id", asyncCatch(async (req, res) => { - res.json(await getOrAddUser(req.params["id"])); +app.get("/private/user/:idOrName", asyncCatch(async (req, res) => { + res.json(await lookupUser(req.params["idOrName"])); })); -app.get( - "/private/user/:id/ban", - asyncCatch(async (req, res) => { - const id = req.params["id"]; - const user = await getOrAddUser(id); - res.send({ banned: user.banned }); - }) -); - app.post( - "/private/user/:id/ban", + "/private/user/:idOrName/ban", asyncCatch(async (req, res) => { - const id = req.params["id"]; - await setUserBanned(id, true); + const user = await lookupUser(req.params["idOrName"]); + if (!user) { + res.sendStatus(404); + return; + } + + await setUserBanned(user, true); res.sendStatus(200); }) ); app.delete( - "/private/user/:id/ban", + "/private/user/:idOrName/ban", asyncCatch(async (req, res) => { - const id = req.params["id"]; - await setUserBanned(id, false); + const user = await lookupUser(req.params["idOrName"]); + if (!user) { + res.sendStatus(404); + return; + } + + await setUserBanned(user, false); res.sendStatus(200); }) ); diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index 9585eb1..e7c3e11 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -99,11 +99,11 @@ export async function saveOrder(order: Order) { export async function getOrAddUser(id: string): Promise { try { - let user = await getUser(id); - if (!user) { - user = await createUser(id); + const [rows] = (await db.query("SELECT * FROM users WHERE id = ?", [id])) as [RowDataPacket[], any]; + if (!rows.length) { + return await createUser(id); } - return user; + return rows[0] as User; } catch (e: any) { console.error("Database query failed (getOrAddUser)"); console.error(e); @@ -111,9 +111,9 @@ export async function getOrAddUser(id: string): Promise { } } -export async function getUser(id: string) : Promise { +export async function lookupUser(idOrName: string) : Promise { try { - const [rows] = (await db.query("SELECT * FROM users WHERE id = ?", [id])) as [RowDataPacket[], any]; + const [rows] = (await db.query("SELECT * FROM users WHERE id = :idOrName OR login LIKE :idOrName OR displayName LIKE :idOrName", {idOrName})) as [RowDataPacket[], any]; if (!rows.length) { return null; } diff --git a/ebs/src/util/twitch.ts b/ebs/src/util/twitch.ts index b7a9019..48d530e 100644 --- a/ebs/src/util/twitch.ts +++ b/ebs/src/util/twitch.ts @@ -1,5 +1,8 @@ import { ApiClient, HelixUser } from "@twurple/api"; import { RefreshingAuthProvider } from "@twurple/auth"; +import { config as dotenv } from "dotenv"; + +dotenv(); const authProvider = new RefreshingAuthProvider({ clientId: process.env.TWITCH_API_CLIENT_ID!, From 697d8ab6784553c84bc5a842f93a0d4df480ebea Mon Sep 17 00:00:00 2001 From: Alexejhero <32238504+Alexejhero@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:36:13 +0300 Subject: [PATCH 5/9] fix stuff i guess --- common/types.ts | 28 +++++--------- ebs/src/index.ts | 6 +-- ebs/src/modules/game/connection.ts | 60 ++++++++++++++++++------------ ebs/src/modules/orders/order.ts | 32 ++++++++-------- ebs/src/util/twitch.ts | 7 +--- logger/src/index.ts | 4 +- logger/src/util/discord.ts | 3 -- 7 files changed, 67 insertions(+), 73 deletions(-) diff --git a/common/types.ts b/common/types.ts index f79ef72..6485e0f 100644 --- a/common/types.ts +++ b/common/types.ts @@ -3,7 +3,7 @@ export const enum LiteralTypes { Integer, Float, Boolean, - Vector + Vector, } type EnumTypeName = string; @@ -56,7 +56,7 @@ export type Redeem = { args: Parameter[]; announce?: boolean; moderated?: boolean; - + image: string; price: number; sku: string; @@ -87,23 +87,15 @@ export type Transaction = { token: string; }; -export type PubSubMessage = ConfigRefreshed | Banned; -export type PubSubMessageBase = { +export type PubSubMessage = { type: "config_refreshed" | "banned"; data: string; }; -export type ConfigRefreshed = PubSubMessageBase & { - type: "config_refreshed"; - data: string; -} -export type Banned = PubSubMessageBase & { - type: "banned"; - data: string; -} + export type BannedData = { id: string; banned: boolean; -} +}; export type LogMessage = { transactionToken: string | null; @@ -113,8 +105,8 @@ export type LogMessage = { }; export type User = { - id: string, - login?: string, - displayName?: string, - banned: boolean, -} + id: string; + login?: string; + displayName?: string; + banned: boolean; +}; diff --git a/ebs/src/index.ts b/ebs/src/index.ts index 2179461..308946d 100644 --- a/ebs/src/index.ts +++ b/ebs/src/index.ts @@ -1,4 +1,4 @@ -import { config as dotenv } from "dotenv"; +import "dotenv/config"; import cors from "cors"; import express from "express"; import expressWs from "express-ws"; @@ -7,8 +7,6 @@ import { asyncCatch, privateApiAuth, publicApiAuth } from "./util/middleware"; import { initDb } from "./util/db"; import { sendToLogger } from "./util/logger"; -dotenv(); - const port = 3000; export const { app } = expressWs(express()); @@ -82,4 +80,4 @@ async function main() { }); } -main().catch(console.error); \ No newline at end of file +main().catch(console.error); diff --git a/ebs/src/modules/game/connection.ts b/ebs/src/modules/game/connection.ts index 0ebafcf..f37ea84 100644 --- a/ebs/src/modules/game/connection.ts +++ b/ebs/src/modules/game/connection.ts @@ -1,10 +1,11 @@ import { Message, MessageType, TwitchUser } from "./messages"; -import { ResultMessage, GameMessage } from "./messages.game"; -import * as ServerWS from "ws"; +import { GameMessage, ResultMessage } from "./messages.game"; +import { WebSocket as ServerWS } from "ws"; import { v4 as uuid } from "uuid"; import { CommandInvocationSource, RedeemMessage, ServerMessage } from "./messages.server"; -import { Order, Redeem } from "common/types"; +import { Redeem } from "common/types"; import { setIngame } from "../config"; +import { Order } from "../orders/order"; const VERSION = "0.1.0"; @@ -34,7 +35,7 @@ export class GameConnection { console.log("Connected to game"); this.handshake = false; this.resendIntervalHandle = +setInterval(() => this.tryResendFromQueue(), this.resendInterval); - ws.on('message', async (message) => { + ws.on("message", async (message) => { const msgText = message.toString(); let msg: GameMessage; try { @@ -43,21 +44,20 @@ export class GameConnection { console.error("Could not parse message" + msgText); return; } - if (msg.messageType !== MessageType.Ping) - console.log(`Got message ${JSON.stringify(msg)}`); + if (msg.messageType !== MessageType.Ping) console.log(`Got message ${JSON.stringify(msg)}`); this.processMessage(msg); }); ws.on("close", (code, reason) => { - const reasonStr = reason ? `reason '${reason}'` : "no reason" + const reasonStr = reason ? `reason '${reason}'` : "no reason"; console.log(`Game socket closed with code ${code} and ${reasonStr}`); setIngame(false); if (this.resendIntervalHandle) { clearInterval(this.resendIntervalHandle); } - }) + }); ws.on("error", (error) => { console.log(`Game socket error\n${error}`); - }) + }); } public processMessage(msg: GameMessage) { switch (msg.messageType) { @@ -66,11 +66,15 @@ export class GameConnection { const reply = { ...this.makeMessage(MessageType.HelloBack), allowed: msg.version == VERSION, - } - this.sendMessage(reply).then().catch(e => e); + }; + this.sendMessage(reply) + .then() + .catch((e) => e); break; case MessageType.Ping: - this.sendMessage(this.makeMessage(MessageType.Pong)).then().catch(e => e); + this.sendMessage(this.makeMessage(MessageType.Pong)) + .then() + .catch((e) => e); break; case MessageType.Result: if (!this.outstandingRedeems.has(msg.guid)) { @@ -116,8 +120,7 @@ export class GameConnection { reject(err); return; } - if (msg.messageType !== MessageType.Pong) - console.debug(`Sent message ${JSON.stringify(msg)}`); + if (msg.messageType !== MessageType.Pong) console.debug(`Sent message ${JSON.stringify(msg)}`); resolve(); }); }); @@ -126,12 +129,17 @@ export class GameConnection { return { messageType: type, guid: guid ?? uuid(), - timestamp: Date.now() - } + timestamp: Date.now(), + }; } - public redeem(redeem: Redeem, order: Order, user: TwitchUser) : Promise { + public redeem(redeem: Redeem, order: Order, user: TwitchUser): Promise { return Promise.race([ - new Promise((_, reject) => setTimeout(() => reject(`Timed out waiting for result. The redeem may still go through later, contact AlexejheroDev if it doesn't.`), GameConnection.resultWaitTimeout)), + new Promise((_, reject) => + setTimeout( + () => reject(`Timed out waiting for result. The redeem may still go through later, contact AlexejheroDev if it doesn't.`), + GameConnection.resultWaitTimeout + ) + ), new Promise((resolve, reject) => { const msg: RedeemMessage = { ...this.makeMessage(MessageType.Redeem), @@ -141,7 +149,7 @@ export class GameConnection { title: redeem.title, announce: redeem.announce ?? true, args: order.cart!.args, - user + user, } as RedeemMessage; if (this.outstandingRedeems.has(msg.guid)) { reject(`Redeeming ${msg.guid} more than once`); @@ -149,9 +157,11 @@ export class GameConnection { } this.outstandingRedeems.set(msg.guid, msg); this.resultHandlers.set(msg.guid, resolve); - - this.sendMessage(msg).then().catch(e => e); // will get queued to re-send later - }) + + this.sendMessage(msg) + .then() + .catch((e) => e); // will get queued to re-send later + }), ]); } @@ -173,7 +183,9 @@ export class GameConnection { } console.log(`Re-sending message ${JSON.stringify(msg)}`); - this.sendMessage(msg).then().catch(e => e); + this.sendMessage(msg) + .then() + .catch((e) => e); } public stressTestSetHandshake(handshake: boolean) { this.handshake = handshake; @@ -192,7 +204,7 @@ export class GameConnection { this.resultHandlers.set(guid, (result: ResultMessage) => { existing(result); resolve(result); - }) + }); } else { this.resultHandlers.set(guid, resolve); } diff --git a/ebs/src/modules/orders/order.ts b/ebs/src/modules/orders/order.ts index c24064b..ad00c31 100644 --- a/ebs/src/modules/orders/order.ts +++ b/ebs/src/modules/orders/order.ts @@ -1,20 +1,20 @@ import { Cart } from "common/types"; -export type OrderState -= "rejected" -| "prepurchase" -| "cancelled" -| "paid" // waiting for game -| "failed" // game failed/timed out -| "succeeded"; +export type OrderState = + | "rejected" + | "prepurchase" + | "cancelled" + | "paid" // waiting for game + | "failed" // game failed/timed out + | "succeeded"; export type Order = { - id: string, - userId: string, - state: OrderState, - cart?: Cart, - receipt?: string, - result?: string, - createdAt: number, - updatedAt: number, -} \ No newline at end of file + id: string; + userId: string; + state: OrderState; + cart?: Cart; + receipt?: string; + result?: string; + createdAt: number; + updatedAt: number; +}; diff --git a/ebs/src/util/twitch.ts b/ebs/src/util/twitch.ts index 48d530e..976e39c 100644 --- a/ebs/src/util/twitch.ts +++ b/ebs/src/util/twitch.ts @@ -1,14 +1,11 @@ import { ApiClient, HelixUser } from "@twurple/api"; import { RefreshingAuthProvider } from "@twurple/auth"; -import { config as dotenv } from "dotenv"; - -dotenv(); const authProvider = new RefreshingAuthProvider({ clientId: process.env.TWITCH_API_CLIENT_ID!, - clientSecret: process.env.TWITCH_API_CLIENT_SECRET! + clientSecret: process.env.TWITCH_API_CLIENT_SECRET!, }); -const api = new ApiClient({authProvider, batchDelay: 100}); +const api = new ApiClient({ authProvider, batchDelay: 100 }); export async function getHelixUser(userId: string): Promise { return api.users.getUserByIdBatched(userId); diff --git a/logger/src/index.ts b/logger/src/index.ts index 37bd0e3..552afb7 100644 --- a/logger/src/index.ts +++ b/logger/src/index.ts @@ -1,11 +1,9 @@ -import { config as dotenv } from "dotenv"; +import "dotenv/config"; import cors from "cors"; import express from "express"; import bodyParser from "body-parser"; import { initDb } from "./util/db"; -dotenv(); - const port = 3000; export const app = express(); diff --git a/logger/src/util/discord.ts b/logger/src/util/discord.ts index 64c2ea9..7f54dcb 100644 --- a/logger/src/util/discord.ts +++ b/logger/src/util/discord.ts @@ -1,9 +1,6 @@ import { Webhook } from "@vermaysha/discord-webhook"; import { LogMessage } from "common/types"; import { stringify } from "./stringify"; -import { config as dotenv } from "dotenv"; - -dotenv(); const hook = new Webhook(process.env.DISCORD_WEBHOOK!); hook.setUsername("Swarm Control"); From 2561529f3abb3bd63dbbbbeca45975b7cac92c53 Mon Sep 17 00:00:00 2001 From: Govorunb Date: Mon, 8 Jul 2024 02:27:40 +1000 Subject: [PATCH 6/9] touch up logger too --- common/types.ts | 19 ++++++ ebs/src/modules/orders/index.ts | 5 +- ebs/src/modules/orders/order.ts | 20 ------ ebs/src/util/db.ts | 3 +- ebs/src/util/jwt.ts | 3 +- frontend/www/src/util/logger.ts | 4 +- logger/src/modules/endpoints.ts | 115 ++++++++++++++++++++++---------- logger/src/util/db.ts | 39 +++-------- 8 files changed, 115 insertions(+), 93 deletions(-) delete mode 100644 ebs/src/modules/orders/order.ts diff --git a/common/types.ts b/common/types.ts index 6485e0f..13bba18 100644 --- a/common/types.ts +++ b/common/types.ts @@ -110,3 +110,22 @@ export type User = { displayName?: string; banned: boolean; }; + +export type OrderState = + | "rejected" + | "prepurchase" + | "cancelled" + | "paid" // waiting for game + | "failed" // game failed/timed out + | "succeeded"; + +export type Order = { + id: string; + userId: string; + state: OrderState; + cart?: Cart; + receipt?: string; + result?: string; + createdAt: number; + updatedAt: number; +}; \ No newline at end of file diff --git a/ebs/src/modules/orders/index.ts b/ebs/src/modules/orders/index.ts index 60513d2..d3a6916 100644 --- a/ebs/src/modules/orders/index.ts +++ b/ebs/src/modules/orders/index.ts @@ -1,4 +1,4 @@ -import { Cart, LogMessage, Transaction } from "common/types"; +import { Cart, LogMessage, Transaction, Order } from "common/types"; import { app } from "../.."; import { parseJWT, verifyJWT } from "../../util/jwt"; import { BitsTransactionPayload } from "../../types"; @@ -11,7 +11,6 @@ import { asyncCatch } from "../../util/middleware"; import { sendShock } from "../../util/pishock"; import { validatePrepurchase } from "./prepurchase"; import { setUserBanned } from "./user"; -import { Order } from "./order"; require('./user'); @@ -99,7 +98,7 @@ app.post( if (!verifyJWT(transaction.receipt)) { logMessage.header = "Invalid receipt"; sendToLogger(logContext).then(); - setUserBanned(req.user.id, true); + setUserBanned(req.user, true); res.status(403).send("Invalid receipt."); return; } diff --git a/ebs/src/modules/orders/order.ts b/ebs/src/modules/orders/order.ts deleted file mode 100644 index ad00c31..0000000 --- a/ebs/src/modules/orders/order.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Cart } from "common/types"; - -export type OrderState = - | "rejected" - | "prepurchase" - | "cancelled" - | "paid" // waiting for game - | "failed" // game failed/timed out - | "succeeded"; - -export type Order = { - id: string; - userId: string; - state: OrderState; - cart?: Cart; - receipt?: string; - result?: string; - createdAt: number; - updatedAt: number; -}; diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index e7c3e11..2bf3331 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -1,8 +1,7 @@ import { RowDataPacket } from "mysql2"; import mysql from "mysql2/promise"; import { v4 as uuid } from "uuid"; -import { Order } from "../modules/orders/order"; -import { User } from "common/types"; +import { User, Order } from "common/types"; import { getTwitchUser } from "../modules/twitch"; export let db: mysql.Connection; diff --git a/ebs/src/util/jwt.ts b/ebs/src/util/jwt.ts index cdb0ce9..b34c9fd 100644 --- a/ebs/src/util/jwt.ts +++ b/ebs/src/util/jwt.ts @@ -22,8 +22,7 @@ export function parseJWT(token: string) { } function getJwtSecretBuffer() { - if (cachedBuffer) return cachedBuffer; - return cachedBuffer = Buffer.from(process.env.JWT_SECRET!, "base64"); + return cachedBuffer ??= Buffer.from(process.env.JWT_SECRET!, "base64"); } export function signJWT(payload: object, buffer: Buffer = getJwtSecretBuffer()) { diff --git a/frontend/www/src/util/logger.ts b/frontend/www/src/util/logger.ts index b93c264..8f8aa7b 100644 --- a/frontend/www/src/util/logger.ts +++ b/frontend/www/src/util/logger.ts @@ -15,12 +15,12 @@ export async function logToDiscord(data: LogMessage) { }); if (!result.ok) { - console.error("Failed to log to Discord"); + console.error("Failed to log to backend"); console.error(await result.text()); console.log(data); } } catch (e: any) { - console.error("Error when logging to Discord"); + console.error("Error when logging to backend"); console.error(e); console.log(data); } diff --git a/logger/src/modules/endpoints.ts b/logger/src/modules/endpoints.ts index 5b168a0..943569d 100644 --- a/logger/src/modules/endpoints.ts +++ b/logger/src/modules/endpoints.ts @@ -1,47 +1,28 @@ import { app } from ".."; import { logToDiscord } from "../util/discord"; -import { LogMessage } from "common/types"; -import { canLog, getUserIdFromTransactionToken, isUserBanned, logToDatabase } from "../util/db"; +import { LogMessage, OrderState } from "common/types"; +import { getOrderById, getUserById, logToDatabase } from "../util/db"; + +// prevent replaying completed transactions +const orderStatesCanLog: { [key in OrderState]: boolean } = { + rejected: false, // completed + prepurchase: true, // idk some js errors or something + cancelled: false, // completed + paid: true, // log timeout response + failed: true, // log error + succeeded: false, // completed +}; +const rejectLogsWithNoToken = true; app.post("/log", async (req, res) => { try { const logMessage = req.body as LogMessage & { backendToken?: string }; const isBackendRequest = process.env.PRIVATE_LOGGER_TOKEN == logMessage.backendToken; - if (!isBackendRequest) { - const validTransactionToken = await canLog(logMessage.transactionToken); - if (!validTransactionToken) { - res.status(403).send("Invalid transaction token."); - return; - } - - // Even if the transaction token is valid, this might be a malicious request using a previously created token. - // In the eventuality that this happens, we also check for extension bans here. - - const userId = await getUserIdFromTransactionToken(logMessage.transactionToken!); - - if (userId && (await isUserBanned(userId))) { - res.status(403).send("User is banned."); - return; - } - - /*if (userId != logMessage.userIdInsecure) { - const warningMessage: LogMessage = { - transactionToken: logMessage.transactionToken, - userIdInsecure: userId, - important: true, - fields: [ - { - header: "Someone tried to bamboozle the logger user id check", - content: "Received user id: " + logMessage.userIdInsecure, - }, - ], - }; - logToDiscord(warningMessage, true); - logToDatabase(warningMessage, true).then(); - res.status(403).send("Invalid user id."); - return; - }*/ + const logDenied = await canLog(logMessage, isBackendRequest); + if (logDenied) { + res.status(logDenied.status).send(logDenied.reason); + return; } await logToDatabase(logMessage, isBackendRequest); @@ -57,3 +38,65 @@ app.post("/log", async (req, res) => { res.status(500).send("Failed to log"); } }); + +type LogDenied = { + status: number; + reason: string; +}; +async function canLog(logMessage: LogMessage, isBackendRequest: boolean): Promise { + if (isBackendRequest) return null; + + if (!logMessage.transactionToken && rejectLogsWithNoToken) return { status: 400, reason: "Invalid transaction token." }; + + const claimedUser = await getUserById(logMessage.userIdInsecure); + if (!claimedUser) { + return { status: 403, reason: "Invalid user id." }; + } + if (claimedUser.banned) { + return { status: 403, reason: "User is banned." }; + } + + const order = await getOrderById(logMessage.transactionToken); + if (!order || !orderStatesCanLog[order.state]) { + return { status: 400, reason: "Invalid transaction token." }; + } + + const errorContext: LogMessage = { + transactionToken: logMessage.transactionToken, + userIdInsecure: logMessage.userIdInsecure, + important: true, + fields: [{ header: "", content: {} }], + }; + const errorMessage = errorContext.fields[0]; + + const user = await getUserById(order.userId); + if (!user) { + errorMessage.header = "Tried to log for order whose userId is not in users table"; + errorMessage.content = { + orderUser: order.userId, + order: order.id, + logMessage, + }; + logToDiscord(errorContext, false); + logToDatabase(errorContext, false).then(); + return { status: 500, reason: "Invalid user id in transaction." }; + } + if (user.id != logMessage.userIdInsecure) { + errorMessage.header = "Someone tried to bamboozle the logger user id check"; + errorMessage.content = { + claimedUser: logMessage.userIdInsecure, + orderUser: user.id, + order: order.id, + logMessage, + }; + logToDiscord(errorContext, false); + logToDatabase(errorContext, false).then(); + return { status: 403, reason: "Invalid user id." }; + } + + if (user.banned) { + return { status: 403, reason: "User is banned." }; + } + + return null; +} diff --git a/logger/src/util/db.ts b/logger/src/util/db.ts index 946cbb2..0c1736b 100644 --- a/logger/src/util/db.ts +++ b/logger/src/util/db.ts @@ -1,5 +1,5 @@ import { RowDataPacket } from "mysql2"; -import { LogMessage, Order } from "common/types"; +import { LogMessage, Order, User } from "common/types"; import { stringify } from "./stringify"; import { logToDiscord } from "./discord"; import mysql from "mysql2/promise"; @@ -38,41 +38,24 @@ async function setupDb() { `); } -export async function canLog(token: string | null): Promise { +async function getById(table: string, id: string | null): Promise { try { - if (!token) return false; - - const [rows] = (await db.query("SELECT * FROM orders WHERE id = ?", [token])) as any; - const order = rows[0] as Order | undefined; - - return order?.state === 0; // OrderState.Prepurchase + if (!id) return null; + const [rows] = (await db.query(`SELECT * FROM ${table} WHERE id = ?`, [id])) as [RowDataPacket[], any]; + return (rows[0] as T) || null; } catch (e: any) { - console.error("Database query failed (canLog)"); + console.error(`Database query failed (getById from ${table})`); console.error(e); - return true; + return null; } } -export async function getUserIdFromTransactionToken(token: string): Promise { - try { - const [rows] = (await db.query("SELECT userId FROM orders WHERE id = ?", [token])) as [RowDataPacket[], any]; - return rows[0].userId; - } catch (e: any) { - console.error("Database query failed (getUserIdFromTransactionToken)"); - console.error(e); - return null; - } +export async function getOrderById(orderId: string | null): Promise { + return getById("orders", orderId); } -export async function isUserBanned(userId: string): Promise { - try { - const [rows] = (await db.query("SELECT banned FROM users WHERE id = ?", [userId])) as [RowDataPacket[], any]; - return rows[0]?.banned; - } catch (e: any) { - console.error("Database query failed (isBanned)"); - console.error(e); - return false; - } +export async function getUserById(userId: string | null): Promise { + return getById("users", userId); } export async function logToDatabase(logMessage: LogMessage, isFromBackend: boolean) { From 2ede74a768953a48be0f869dd86f4204fadeb664 Mon Sep 17 00:00:00 2001 From: Govorunb Date: Mon, 8 Jul 2024 02:51:29 +1000 Subject: [PATCH 7/9] db init --- docker-compose.yml | 1 + ebs/src/util/db.ts | 25 ------------------------- logger/src/util/db.ts | 15 --------------- scripts/sql/init_db.sql | 28 ++++++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 40 deletions(-) create mode 100644 scripts/sql/init_db.sql diff --git a/docker-compose.yml b/docker-compose.yml index 4073aa4..4e864d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,7 @@ services: MYSQL_PASSWORD: ebs volumes: - ./_volumes/db:/var/lib/mysql + - ./scripts/sql/init_db.sql:/docker-entrypoint-initdb.d/init_db.sql nginx-proxy: image: nginxproxy/nginx-proxy:latest diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index 2bf3331..6df9819 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -21,31 +21,6 @@ export async function initDb() { await new Promise((resolve) => setTimeout(resolve, 5000)); } } - - await setupDb(); -} - -async function setupDb() { - await db.query(` - CREATE TABLE IF NOT EXISTS users ( - id VARCHAR(255) PRIMARY KEY, - login VARCHAR(255), - displayName VARCHAR(255), - banned BOOLEAN - ); - `); - await db.query(` - CREATE TABLE IF NOT EXISTS orders ( - id VARCHAR(36) PRIMARY KEY, - userId VARCHAR(255) NOT NULL, - state ENUM('rejected', 'prepurchase', 'cancelled', 'paid', 'failed', 'succeeded'), - cart JSON, - receipt VARCHAR(1024), - result TEXT, - createdAt BIGINT, - updatedAt BIGINT - ); - `); } export async function getOrder(guid: string) { diff --git a/logger/src/util/db.ts b/logger/src/util/db.ts index 0c1736b..819fc67 100644 --- a/logger/src/util/db.ts +++ b/logger/src/util/db.ts @@ -21,21 +21,6 @@ export async function initDb() { await new Promise((resolve) => setTimeout(resolve, 5000)); } } - - await setupDb(); -} - -async function setupDb() { - await db.query(` - CREATE TABLE IF NOT EXISTS logs ( - id INT PRIMARY KEY AUTO_INCREMENT, - userId VARCHAR(255), - transactionToken VARCHAR(255), - data TEXT NOT NULL, - fromBackend BOOLEAN NOT NULL, - timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `); } async function getById(table: string, id: string | null): Promise { diff --git a/scripts/sql/init_db.sql b/scripts/sql/init_db.sql new file mode 100644 index 0000000..a037ba4 --- /dev/null +++ b/scripts/sql/init_db.sql @@ -0,0 +1,28 @@ +USE ebs; + +CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(255) PRIMARY KEY, + login VARCHAR(255), + displayName VARCHAR(255), + banned BOOLEAN +); + +CREATE TABLE IF NOT EXISTS orders ( + id VARCHAR(36) PRIMARY KEY, + userId VARCHAR(255) NOT NULL, + state ENUM('rejected', 'prepurchase', 'cancelled', 'paid', 'failed', 'succeeded'), + cart JSON, + receipt VARCHAR(1024), + result TEXT, + createdAt BIGINT, + updatedAt BIGINT +); + +CREATE TABLE IF NOT EXISTS logs ( + id INT PRIMARY KEY AUTO_INCREMENT, + userId VARCHAR(255), + transactionToken VARCHAR(255), + data TEXT NOT NULL, + fromBackend BOOLEAN NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); From a98010425a83bb86501b9f65d9ce55cd76e425f3 Mon Sep 17 00:00:00 2001 From: Govorunb Date: Mon, 8 Jul 2024 03:51:42 +1000 Subject: [PATCH 8/9] fix the fix --- ebs/src/modules/game/connection.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ebs/src/modules/game/connection.ts b/ebs/src/modules/game/connection.ts index f37ea84..97ae706 100644 --- a/ebs/src/modules/game/connection.ts +++ b/ebs/src/modules/game/connection.ts @@ -1,11 +1,10 @@ import { Message, MessageType, TwitchUser } from "./messages"; import { GameMessage, ResultMessage } from "./messages.game"; -import { WebSocket as ServerWS } from "ws"; +import * as ServerWS from "ws"; import { v4 as uuid } from "uuid"; import { CommandInvocationSource, RedeemMessage, ServerMessage } from "./messages.server"; -import { Redeem } from "common/types"; +import { Redeem, Order } from "common/types"; import { setIngame } from "../config"; -import { Order } from "../orders/order"; const VERSION = "0.1.0"; From f967bf48f896197db4b7e672a3165025ac3af31a Mon Sep 17 00:00:00 2001 From: Govorunb Date: Mon, 8 Jul 2024 05:33:32 +1000 Subject: [PATCH 9/9] oop --- ebs/src/modules/orders/index.ts | 2 ++ ebs/src/modules/twitch.ts | 3 ++- ebs/src/util/db.ts | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ebs/src/modules/orders/index.ts b/ebs/src/modules/orders/index.ts index d3a6916..ce9ebd4 100644 --- a/ebs/src/modules/orders/index.ts +++ b/ebs/src/modules/orders/index.ts @@ -197,6 +197,8 @@ app.post( if (!req.user.login || !req.user.displayName) { try { await updateUserTwitchInfo(req.user); + userInfo.login = req.user.login!; + userInfo.displayName = req.user.displayName!; } catch (error) { logContext.important = true; logMessage.header = "Could not get Twitch user info"; diff --git a/ebs/src/modules/twitch.ts b/ebs/src/modules/twitch.ts index 93499a7..fdfcaf9 100644 --- a/ebs/src/modules/twitch.ts +++ b/ebs/src/modules/twitch.ts @@ -4,11 +4,12 @@ import { TwitchUser } from "./game/messages"; export async function getTwitchUser(id: string): Promise { const user = await getHelixUser(id); if (!user) { + console.warn(`Twitch user ${id} was not found`); return null; } return { id: user.id, - displayName: user.displayName, login: user.name, + displayName: user.displayName, }; } \ No newline at end of file diff --git a/ebs/src/util/db.ts b/ebs/src/util/db.ts index 6df9819..3a215ac 100644 --- a/ebs/src/util/db.ts +++ b/ebs/src/util/db.ts @@ -67,7 +67,7 @@ export async function saveOrder(order: Order) { SET state = ?, cart = ?, receipt = ?, result = ?, updatedAt = ? WHERE id = ? `, - [order.state, JSON.stringify(order.cart), order.receipt, order.receipt, order.updatedAt, order.id] + [order.state, JSON.stringify(order.cart), order.receipt, order.result, order.updatedAt, order.id] ); } @@ -135,7 +135,7 @@ export async function saveUser(user: User) { } } -export async function updateUserTwitchInfo(user: User) { +export async function updateUserTwitchInfo(user: User): Promise { try { user = { ...user, @@ -159,4 +159,5 @@ export async function updateUserTwitchInfo(user: User) { console.error(e); throw e; } + return user; }