Compare commits
20 Commits
b673f6fcb3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 438bb4c8a3 | |||
| 97ed3c4d9e | |||
| 3861b4477d | |||
| 0fe2781a11 | |||
| be7b0c48e3 | |||
| 3331c31d22 | |||
| 20efdb5dc2 | |||
| 50a1631222 | |||
| d67c8b5ac1 | |||
| 683eabecdd | |||
| 16cba64d03 | |||
| 89ef1d6e8b | |||
| 927a1b75c6 | |||
| 0aa2ae1637 | |||
| f638c4709b | |||
| e97cede55c | |||
| 6e0a97df53 | |||
| f10714b2c8 | |||
| fcbe008a5c | |||
| 32db9278e4 |
@@ -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
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
@@ -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
1096
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
schema.sql
29
schema.sql
@@ -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)
|
||||||
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
33
src/index.js
33
src/index.js
@@ -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();
|
||||||
|
|||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user