Compare commits

..

20 Commits

Author SHA1 Message Date
438bb4c8a3 Add roundStartsAt property to Table class and update state management 2025-12-21 02:54:09 +01:00
97ed3c4d9e Add promo code functionality to deposits and enhance wallet routes 2025-12-21 00:41:39 +01:00
3861b4477d Update deposit intent to log stripeAmount and use it for payment creation 2025-12-21 00:32:36 +01:00
0fe2781a11 Add logging for Stripe deposit requests and round amount input 2025-12-21 00:29:33 +01:00
be7b0c48e3 Refactor MIN_DEPOSIT calculation to ensure it respects environment variable settings 2025-12-21 00:24:56 +01:00
3331c31d22 Update wallet route to use environment variables for deposit limits 2025-12-21 00:21:06 +01:00
20efdb5dc2 Add ensureStripeConfigured function and improve error handling in wallet routes 2025-12-21 00:16:56 +01:00
50a1631222 Ensure dotenv is imported for environment variable configuration 2025-12-21 00:10:42 +01:00
d67c8b5ac1 Add logging for database connection status and host 2025-12-21 00:09:28 +01:00
683eabecdd Remove redundant dotenv import from index.js 2025-12-21 00:08:42 +01:00
16cba64d03 Refactor code structure for improved readability and maintainability 2025-12-21 00:08:25 +01:00
89ef1d6e8b Add logging for SQL execution and database host in query function 2025-12-21 00:06:23 +01:00
927a1b75c6 Refactor dotenv configuration to load environment variables from multiple candidates; add error handling for missing DB_HOST 2025-12-21 00:04:02 +01:00
0aa2ae1637 Update dotenv configuration to specify environment file path 2025-12-21 00:01:41 +01:00
f638c4709b Refactor database connection logic; ensure DB is connected before starting the server 2025-12-21 00:00:46 +01:00
e97cede55c Improve error handling in Discord auth callback; return JSON error response in non-production environments 2025-12-20 23:48:34 +01:00
6e0a97df53 Add turn timer functionality to Table class; update environment configuration 2025-12-20 23:33:55 +01:00
f10714b2c8 Remove Stripe integration and related routes; update wallet routes for payment intent confirmation 2025-12-20 23:14:59 +01:00
fcbe008a5c Refactor betting logic and add error handling in Table class; implement async turn advancement 2025-12-20 23:10:03 +01:00
32db9278e4 Update MAX_BET value to 500 in .env.example 2025-12-20 23:03:44 +01:00
11 changed files with 1381 additions and 75 deletions

View File

@@ -15,9 +15,11 @@ DISCORD_REDIRECT_URI=http://localhost:4000/auth/discord/callback
DEFAULT_APP_REDIRECT=blackjack://auth DEFAULT_APP_REDIRECT=blackjack://auth
STRIPE_SECRET_KEY=sk_test_... STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
MIN_BET=10 MIN_BET=10
MAX_BET=100 MAX_BET=500
ROUND_START_DELAY_MS=3000 ROUND_START_DELAY_MS=3000
ROUND_RESET_DELAY_MS=5000 ROUND_RESET_DELAY_MS=5000
TURN_TIME_MS=15000
MIN_DEPOSIT=175
MAX_DEPOSIT=1000

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.env
node_modules
dist
.DS_Store
.env.local

View File

@@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.6.1",
"express": "^4.19.2", "express": "^4.19.2",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",

1096
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,42 @@ CREATE TABLE IF NOT EXISTS users (
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS promo_codes (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(32) NOT NULL UNIQUE,
bonus_type VARCHAR(16) NOT NULL DEFAULT 'fixed',
bonus_value INT NOT NULL DEFAULT 0,
max_uses INT NULL,
expires_at DATETIME NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS deposits ( CREATE TABLE IF NOT EXISTS deposits (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
amount INT NOT NULL, amount INT NOT NULL,
bonus_amount INT NOT NULL DEFAULT 0,
promo_code_id BIGINT UNSIGNED NULL,
stripe_payment_intent_id VARCHAR(128) NOT NULL, stripe_payment_intent_id VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'created', status VARCHAR(32) NOT NULL DEFAULT 'created',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_deposits_user_id FOREIGN KEY (user_id) REFERENCES users(id) CONSTRAINT fk_deposits_user_id FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_deposits_promo_code_id FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id)
); );
CREATE INDEX idx_deposits_stripe ON deposits (stripe_payment_intent_id); CREATE INDEX idx_deposits_stripe ON deposits (stripe_payment_intent_id);
CREATE TABLE IF NOT EXISTS promo_redemptions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
promo_code_id BIGINT UNSIGNED NOT NULL,
deposit_id BIGINT UNSIGNED NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_redemptions_user_id FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_redemptions_promo_code_id FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id),
CONSTRAINT fk_redemptions_deposit_id FOREIGN KEY (deposit_id) REFERENCES deposits(id),
UNIQUE KEY uniq_user_promo (user_id, promo_code_id)
);

View File

@@ -1,3 +1,4 @@
import 'dotenv/config';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
const pool = mysql.createPool({ const pool = mysql.createPool({
@@ -12,6 +13,9 @@ const pool = mysql.createPool({
}); });
export async function query(sql, params = []) { export async function query(sql, params = []) {
console.log('Executing SQL:', sql, 'with params:', params);
console.log('Database Host:', process.env.DB_HOST);
const [rows] = await pool.execute(sql, params); const [rows] = await pool.execute(sql, params);
return rows; return rows;
} }

View File

@@ -5,6 +5,7 @@ const MIN_BET = Number(process.env.MIN_BET || 10);
const MAX_BET = Number(process.env.MAX_BET || 100); const MAX_BET = Number(process.env.MAX_BET || 100);
const ROUND_START_DELAY_MS = Number(process.env.ROUND_START_DELAY_MS || 3000); const ROUND_START_DELAY_MS = Number(process.env.ROUND_START_DELAY_MS || 3000);
const ROUND_RESET_DELAY_MS = Number(process.env.ROUND_RESET_DELAY_MS || 5000); const ROUND_RESET_DELAY_MS = Number(process.env.ROUND_RESET_DELAY_MS || 5000);
const TURN_TIME_MS = Number(process.env.TURN_TIME_MS || 15000);
class Table { class Table {
constructor(id, seatCount) { constructor(id, seatCount) {
@@ -26,6 +27,10 @@ class Table {
this.roundId = 0; this.roundId = 0;
this.roundTimeout = null; this.roundTimeout = null;
this.resetTimeout = null; this.resetTimeout = null;
this.turnTimeout = null;
this.turnEndsAt = null;
this.turnCounter = 0;
this.roundStartsAt = null;
this.clients = new Set(); this.clients = new Set();
} }
@@ -55,6 +60,8 @@ class Table {
minBet: MIN_BET, minBet: MIN_BET,
maxBet: MAX_BET, maxBet: MAX_BET,
roundId: this.roundId, roundId: this.roundId,
turnEndsAt: this.turnEndsAt,
roundStartsAt: this.roundStartsAt,
dealerHand, dealerHand,
currentSeatIndex: this.currentSeatIndex, currentSeatIndex: this.currentSeatIndex,
seats: this.seats.map((seat, index) => ({ seats: this.seats.map((seat, index) => ({
@@ -103,6 +110,36 @@ class Table {
} }
} }
clearTurnTimer() {
if (this.turnTimeout) {
clearTimeout(this.turnTimeout);
}
this.turnTimeout = null;
this.turnEndsAt = null;
}
startTurnTimer() {
this.clearTurnTimer();
if (this.currentSeatIndex === null) {
return;
}
this.turnEndsAt = Date.now() + TURN_TIME_MS;
const turnToken = ++this.turnCounter;
this.turnTimeout = setTimeout(() => {
if (this.turnCounter !== turnToken) {
return;
}
const seat = this.seats[this.currentSeatIndex];
if (!seat || seat.status !== 'playing') {
return;
}
seat.status = 'stood';
void this.advanceTurn();
}, TURN_TIME_MS);
}
findSeatIndexByUser(userId) { findSeatIndexByUser(userId) {
return this.seats.findIndex((seat) => seat.userId === userId); return this.seats.findIndex((seat) => seat.userId === userId);
} }
@@ -158,7 +195,7 @@ class Table {
}; };
if (this.currentSeatIndex === index) { if (this.currentSeatIndex === index) {
this.advanceTurn(); await this.advanceTurn();
} }
if (this.seats.every((seat) => !seat.userId)) { if (this.seats.every((seat) => !seat.userId)) {
@@ -169,7 +206,7 @@ class Table {
} }
async placeBet(userId, amount) { async placeBet(userId, amount) {
if (this.phase === 'playing') { if (!['waiting', 'betting'].includes(this.phase)) {
throw new Error('A kor mar fut, varj a kovetkezo korig.'); throw new Error('A kor mar fut, varj a kovetkezo korig.');
} }
@@ -182,6 +219,11 @@ class Table {
throw new Error('Nem ulsz az asztalnal.'); throw new Error('Nem ulsz az asztalnal.');
} }
const seat = this.seats[index];
if (seat.bet > 0) {
throw new Error('Mar van teted erre a korre.');
}
const balanceRows = await query('SELECT balance FROM users WHERE id = ?', [userId]); const balanceRows = await query('SELECT balance FROM users WHERE id = ?', [userId]);
const balance = balanceRows[0]?.balance ?? 0; const balance = balanceRows[0]?.balance ?? 0;
if (balance < amount) { if (balance < amount) {
@@ -192,7 +234,6 @@ class Table {
const updatedBalanceRows = await query('SELECT balance FROM users WHERE id = ?', [userId]); const updatedBalanceRows = await query('SELECT balance FROM users WHERE id = ?', [userId]);
const updatedBalance = updatedBalanceRows[0]?.balance ?? 0; const updatedBalance = updatedBalanceRows[0]?.balance ?? 0;
const seat = this.seats[index];
seat.bet = amount; seat.bet = amount;
seat.ready = false; seat.ready = false;
seat.status = 'bet'; seat.status = 'bet';
@@ -218,6 +259,10 @@ class Table {
throw new Error('Eloszor tegyel tetet.'); throw new Error('Eloszor tegyel tetet.');
} }
if (this.phase !== 'betting') {
throw new Error('A kor mar elindult.');
}
seat.ready = true; seat.ready = true;
this.broadcastState(); this.broadcastState();
this.scheduleRound(); this.scheduleRound();
@@ -228,10 +273,14 @@ class Table {
return; return;
} }
this.roundStartsAt = Date.now() + ROUND_START_DELAY_MS;
this.broadcastState();
this.roundTimeout = setTimeout(() => { this.roundTimeout = setTimeout(() => {
this.roundTimeout = null; this.roundTimeout = null;
const hasBets = this.seats.some((seat) => seat.bet > 0); const hasBets = this.seats.some((seat) => seat.bet > 0);
if (!hasBets) { if (!hasBets) {
this.roundStartsAt = null;
this.broadcastState();
return; return;
} }
void this.startRound(); void this.startRound();
@@ -243,6 +292,8 @@ class Table {
return; return;
} }
this.clearTurnTimer();
this.roundStartsAt = null;
this.phase = 'playing'; this.phase = 'playing';
this.roundId += 1; this.roundId += 1;
this.deck = shuffle(createDeck()); this.deck = shuffle(createDeck());
@@ -264,6 +315,7 @@ class Table {
return; return;
} }
this.startTurnTimer();
this.broadcastState(); this.broadcastState();
} }
@@ -298,14 +350,16 @@ class Table {
const value = handValue(seat.hand).total; const value = handValue(seat.hand).total;
if (value > 21) { if (value > 21) {
seat.status = 'bust'; seat.status = 'bust';
this.advanceTurn(); await this.advanceTurn();
} else if (value === 21) { } else if (value === 21) {
seat.status = 'stood'; seat.status = 'stood';
this.advanceTurn(); await this.advanceTurn();
} else {
this.startTurnTimer();
} }
} else if (action === 'stand') { } else if (action === 'stand') {
seat.status = 'stood'; seat.status = 'stood';
this.advanceTurn(); await this.advanceTurn();
} else if (action === 'double') { } else if (action === 'double') {
if (seat.hand.length !== 2) { if (seat.hand.length !== 2) {
throw new Error('Duplazni csak az elso ket lap utan lehet.'); throw new Error('Duplazni csak az elso ket lap utan lehet.');
@@ -323,7 +377,7 @@ class Table {
seat.bet += seat.bet; seat.bet += seat.bet;
seat.hand.push(draw(this.deck)); seat.hand.push(draw(this.deck));
seat.status = 'stood'; seat.status = 'stood';
this.advanceTurn(); await this.advanceTurn();
this.broadcast({ type: 'balance', balance: updatedBalance }, userId); this.broadcast({ type: 'balance', balance: updatedBalance }, userId);
} else { } else {
throw new Error('Ismeretlen akcio.'); throw new Error('Ismeretlen akcio.');
@@ -335,15 +389,18 @@ class Table {
async advanceTurn() { async advanceTurn() {
const nextIndex = this.nextPlayableSeatIndex(this.currentSeatIndex ?? -1); const nextIndex = this.nextPlayableSeatIndex(this.currentSeatIndex ?? -1);
if (nextIndex === null) { if (nextIndex === null) {
this.clearTurnTimer();
await this.dealerTurn(); await this.dealerTurn();
return; return;
} }
this.currentSeatIndex = nextIndex; this.currentSeatIndex = nextIndex;
this.startTurnTimer();
this.broadcastState(); this.broadcastState();
} }
async dealerTurn() { async dealerTurn() {
this.clearTurnTimer();
this.phase = 'dealer'; this.phase = 'dealer';
this.currentSeatIndex = null; this.currentSeatIndex = null;
@@ -415,6 +472,8 @@ class Table {
} }
resetRound() { resetRound() {
this.clearTurnTimer();
this.roundStartsAt = null;
for (const seat of this.seats) { for (const seat of this.seats) {
if (!seat.userId) { if (!seat.userId) {
continue; continue;
@@ -433,7 +492,7 @@ class Table {
} }
} }
const tables = Array.from({ length: 3 }, (_, index) => new Table(index + 1, 7)); const tables = Array.from({ length: 3 }, (_, index) => new Table(index + 1, 4));
export function listTables() { export function listTables() {
return tables.map((table) => table.snapshot()); return tables.map((table) => table.snapshot());

View File

@@ -8,9 +8,8 @@ import { WebSocketServer } from 'ws';
import authRoutes from './routes/auth.js'; import authRoutes from './routes/auth.js';
import lobbyRoutes from './routes/lobby.js'; import lobbyRoutes from './routes/lobby.js';
import walletRoutes from './routes/wallet.js'; import walletRoutes from './routes/wallet.js';
import stripeRoutes from './routes/stripe.js'; import pool from './db.js';
import { setupWebSocket } from './ws.js'; import { setupWebSocket } from './ws.js';
dotenv.config(); dotenv.config();
const app = express(); const app = express();
@@ -19,7 +18,6 @@ app.use(helmet());
app.use(cors({ origin: process.env.CORS_ORIGIN || '*', credentials: true })); app.use(cors({ origin: process.env.CORS_ORIGIN || '*', credentials: true }));
app.use(morgan('dev')); app.use(morgan('dev'));
app.use(stripeRoutes);
app.use(express.json()); app.use(express.json());
app.get('/health', (req, res) => res.json({ ok: true })); app.get('/health', (req, res) => res.json({ ok: true }));
@@ -33,7 +31,28 @@ const wss = new WebSocketServer({ server });
setupWebSocket(wss); setupWebSocket(wss);
const PORT = Number(process.env.PORT || 4000); const PORT = Number(process.env.PORT || 4000);
server.listen(PORT, () => {
// eslint-disable-next-line no-console async function ensureDbConnected() {
console.log(`Backend fut: http://localhost:${PORT}`); try {
}); console.log('Ellenorizzuk az adatbazis kapcsolatot...');
console.log('HOST:', process.env.DB_HOST);
const connection = await pool.getConnection();
await connection.ping();
connection.release();
} catch (err) {
// eslint-disable-next-line no-console
console.error('DB kapcsolat hiba inditas kozben:', err);
process.exit(1);
}
}
async function start() {
await ensureDbConnected();
server.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`Backend fut: http://localhost:${PORT}`);
});
}
start();

View File

@@ -73,6 +73,11 @@ router.get('/auth/discord/callback', async (req, res) => {
return res.json({ token }); return res.json({ token });
} catch (err) { } catch (err) {
console.error('Discord auth hiba:', err);
const message = err?.message || 'Hiba a bejelentkezes kozben.';
if (process.env.NODE_ENV !== 'production') {
return res.status(500).json({ error: message });
}
return res.status(500).send('Hiba a bejelentkezes kozben.'); return res.status(500).send('Hiba a bejelentkezes kozben.');
} }
}); });

View File

@@ -1,49 +0,0 @@
import express, { Router } from 'express';
import Stripe from 'stripe';
import { query } from '../db.js';
const router = Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2024-06-20'
});
router.post('/api/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET || ''
);
} catch (err) {
return res.status(400).send('Webhook alairas hiba.');
}
if (event.type === 'payment_intent.succeeded') {
const intent = event.data.object;
const amount = Number(intent.amount || 0);
const userId = Number(intent.metadata?.userId || 0);
if (userId && amount) {
const rows = await query(
'SELECT status FROM deposits WHERE stripe_payment_intent_id = ?',
[intent.id]
);
const status = rows[0]?.status;
if (status !== 'succeeded') {
await query(
'UPDATE deposits SET status = ? WHERE stripe_payment_intent_id = ?',
['succeeded', intent.id]
);
await query('UPDATE users SET balance = balance + ? WHERE id = ?', [amount, userId]);
}
}
}
return res.json({ received: true });
});
export default router;

View File

@@ -7,16 +7,91 @@ const router = Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2024-06-20' apiVersion: '2024-06-20'
}); });
const STRIPE_MIN_HUF = 175;
const MIN_DEPOSIT = Math.max(Number(process.env.MIN_DEPOSIT || STRIPE_MIN_HUF), STRIPE_MIN_HUF);
const MAX_DEPOSIT = Number(process.env.MAX_DEPOSIT || 1000);
function ensureStripeConfigured(res) {
if (!process.env.STRIPE_SECRET_KEY) {
res.status(500).json({ error: 'Stripe nincs beallitva.' });
return false;
}
return true;
}
async function getPromoCode(code) {
const rows = await query(
'SELECT id, code, bonus_type, bonus_value, max_uses, expires_at, active FROM promo_codes WHERE code = ? AND active = 1 AND (expires_at IS NULL OR expires_at > NOW())',
[code]
);
return rows[0];
}
function calcBonusAmount(amount, promo) {
if (!promo) {
return 0;
}
if (promo.bonus_type === 'percent') {
return Math.max(0, Math.floor(amount * Number(promo.bonus_value) / 100));
}
if (promo.bonus_type === 'fixed') {
return Math.max(0, Number(promo.bonus_value));
}
return 0;
}
router.post('/api/wallet/deposit-intent', authMiddleware, async (req, res) => { router.post('/api/wallet/deposit-intent', authMiddleware, async (req, res) => {
try { try {
const amount = Number(req.body.amount); if (!ensureStripeConfigured(res)) {
if (!Number.isFinite(amount) || amount < 50 || amount > 100) { return;
return res.status(400).json({ error: 'A feltoltes 50 es 100 Ft kozott lehet.' }); }
const rawAmount = req.body.amount;
const amount = Math.round(Number(rawAmount));
const stripeAmount = Math.round(amount * 100);
if (process.env.NODE_ENV !== 'production') {
console.log('Stripe deposit request:', { rawAmount, amount, stripeAmount, min: MIN_DEPOSIT, max: MAX_DEPOSIT });
}
if (!Number.isFinite(amount) || amount < MIN_DEPOSIT || amount > MAX_DEPOSIT) {
return res.status(400).json({ error: `A feltoltes ${MIN_DEPOSIT} es ${MAX_DEPOSIT} Ft kozott lehet.` });
}
const promoInput = req.body.promoCode?.toString().trim().toUpperCase();
let promo = null;
let bonusAmount = 0;
let promoCodeId = null;
if (promoInput) {
promo = await getPromoCode(promoInput);
if (!promo) {
return res.status(400).json({ error: 'Ervenytelen promo kod.' });
}
const redeemed = await query(
'SELECT id FROM promo_redemptions WHERE user_id = ? AND promo_code_id = ? LIMIT 1',
[req.userId, promo.id]
);
if (redeemed.length > 0) {
return res.status(400).json({ error: 'A promo kod mar felhasznalva.' });
}
if (promo.max_uses) {
const usage = await query(
'SELECT COUNT(*) AS count FROM promo_redemptions WHERE promo_code_id = ?',
[promo.id]
);
if ((usage[0]?.count ?? 0) >= promo.max_uses) {
return res.status(400).json({ error: 'A promo kod mar elfogyott.' });
}
}
bonusAmount = calcBonusAmount(amount, promo);
promoCodeId = promo.id;
} }
const paymentIntent = await stripe.paymentIntents.create({ const paymentIntent = await stripe.paymentIntents.create({
amount, amount: stripeAmount,
currency: 'huf', currency: 'huf',
automatic_payment_methods: { enabled: true }, automatic_payment_methods: { enabled: true },
metadata: { metadata: {
@@ -25,14 +100,77 @@ router.post('/api/wallet/deposit-intent', authMiddleware, async (req, res) => {
}); });
await query( await query(
'INSERT INTO deposits (user_id, amount, stripe_payment_intent_id, status) VALUES (?, ?, ?, ?)', 'INSERT INTO deposits (user_id, amount, bonus_amount, promo_code_id, stripe_payment_intent_id, status) VALUES (?, ?, ?, ?, ?, ?)',
[req.userId, amount, paymentIntent.id, 'created'] [req.userId, amount, bonusAmount, promoCodeId, paymentIntent.id, 'created']
); );
return res.json({ clientSecret: paymentIntent.client_secret }); return res.json({ clientSecret: paymentIntent.client_secret, paymentIntentId: paymentIntent.id });
} catch (err) { } catch (err) {
console.error('Stripe fizetes hiba:', err);
if (process.env.NODE_ENV !== 'production') {
return res.status(500).json({ error: err.message || 'Nem sikerult letrehozni a fizetest.' });
}
return res.status(500).json({ error: 'Nem sikerult letrehozni a fizetest.' }); return res.status(500).json({ error: 'Nem sikerult letrehozni a fizetest.' });
} }
}); });
router.post('/api/wallet/confirm', authMiddleware, async (req, res) => {
try {
if (!ensureStripeConfigured(res)) {
return;
}
const paymentIntentId = req.body.paymentIntentId?.toString();
if (!paymentIntentId) {
return res.status(400).json({ error: 'Hianyzo paymentIntentId.' });
}
const rows = await query(
'SELECT id, status, amount, bonus_amount, promo_code_id FROM deposits WHERE user_id = ? AND stripe_payment_intent_id = ?',
[req.userId, paymentIntentId]
);
const deposit = rows[0];
if (!deposit) {
return res.status(404).json({ error: 'Ismeretlen befizetes.' });
}
const intent = await stripe.paymentIntents.retrieve(paymentIntentId);
if (intent.status !== 'succeeded') {
return res.json({ status: intent.status });
}
if (deposit.status !== 'succeeded') {
await query(
'UPDATE deposits SET status = ? WHERE id = ?',
['succeeded', deposit.id]
);
const totalCredit = Number(deposit.amount) + Number(deposit.bonus_amount || 0);
await query('UPDATE users SET balance = balance + ? WHERE id = ?', [totalCredit, req.userId]);
if (deposit.promo_code_id) {
const already = await query(
'SELECT id FROM promo_redemptions WHERE user_id = ? AND promo_code_id = ? LIMIT 1',
[req.userId, deposit.promo_code_id]
);
if (already.length === 0) {
await query(
'INSERT INTO promo_redemptions (user_id, promo_code_id, deposit_id) VALUES (?, ?, ?)',
[req.userId, deposit.promo_code_id, deposit.id]
);
}
}
}
const balanceRows = await query('SELECT balance FROM users WHERE id = ?', [req.userId]);
const balance = balanceRows[0]?.balance ?? 0;
return res.json({ status: 'succeeded', balance });
} catch (err) {
console.error('Stripe confirm hiba:', err);
if (process.env.NODE_ENV !== 'production') {
return res.status(500).json({ error: err.message || 'Nem sikerult a befizetes ellenorzese.' });
}
return res.status(500).json({ error: 'Nem sikerult a befizetes ellenorzese.' });
}
});
export default router; export default router;