v1.0.0 - Initial release: registration, SendGrid email, Square payments, cashout, admin panel

This commit is contained in:
2026-05-10 14:45:49 -05:00
commit c70027f8fc
61 changed files with 11762 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/mailer.php';
function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
function requireLogin(): void {
if (!isLoggedIn()) {
header('Location: /'); exit;
}
}
function requireAdmin(): void {
requireLogin();
if (empty($_SESSION['is_admin'])) {
header('Location: /'); exit;
}
}
function currentUser(): ?array {
if (!isLoggedIn()) return null;
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch() ?: null;
}
function loginUser(string $username, string $password): array {
$stmt = db()->prepare("SELECT * FROM users WHERE username = ? AND status = 'active'");
$stmt->execute([strtolower(trim($username))]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
return ['success' => false, 'error' => 'Invalid username or password.'];
}
// Block unverified accounts — admins are always exempt
if (!$user['email_verified'] && !$user['is_admin']) {
return [
'success' => false,
'error' => 'Please verify your email address before logging in. Check your inbox for the verification link.',
'unverified' => true,
'email' => $user['email'],
];
}
// Auto-verify admin accounts so they're never locked out
if ($user['is_admin'] && !$user['email_verified']) {
db()->prepare("UPDATE users SET email_verified=1 WHERE id=?")->execute([$user['id']]);
$user['email_verified'] = 1;
}
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['alias'] = $user['alias'];
$_SESSION['is_admin'] = $user['is_admin'];
db()->prepare("UPDATE users SET last_login=NOW() WHERE id=?")->execute([$user['id']]);
return ['success' => true, 'user' => $user];
}
/**
* Stage a registration: store in pending_registrations, send verification email.
* Does NOT create the user row yet.
*/
function initiateRegistration(string $username, string $password, string $alias, string $email, string $referralCode = ''): array {
$username = strtolower(trim($username));
$alias = trim($alias);
$email = strtolower(trim($email));
// Validate
if (strlen($username) < 3 || strlen($username) > 50)
return ['success' => false, 'error' => 'Username must be 350 characters.'];
if (!preg_match('/^[a-z0-9_]+$/', $username))
return ['success' => false, 'error' => 'Username may only contain letters, numbers, and underscores.'];
if (strlen($password) < 6)
return ['success' => false, 'error' => 'Password must be at least 6 characters.'];
if (empty($alias))
return ['success' => false, 'error' => 'Alias is required.'];
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL))
return ['success' => false, 'error' => 'A valid email address is required to verify your account.'];
// Check username taken (existing users)
$s = db()->prepare("SELECT id FROM users WHERE username=?");
$s->execute([$username]);
if ($s->fetch()) return ['success' => false, 'error' => 'Username already taken.'];
// Check email taken (existing users)
$s = db()->prepare("SELECT id FROM users WHERE email=?");
$s->execute([$email]);
if ($s->fetch()) return ['success' => false, 'error' => 'An account with that email already exists.'];
// Check username in pending
$s = db()->prepare("SELECT id FROM pending_registrations WHERE username=? AND expires_at > NOW()");
$s->execute([$username]);
if ($s->fetch()) return ['success' => false, 'error' => 'Username already reserved. Try again in 24 hours or choose another.'];
// Check email in pending — resend if already pending
$s = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW()");
$s->execute([$email]);
$existing = $s->fetch();
if ($existing) {
// Resend verification to same email
sendVerificationEmail($email, $alias, $existing['token']);
return ['success' => true, 'resent' => true, 'email' => $email];
}
// Delete any expired pending rows for this email/username
db()->prepare("DELETE FROM pending_registrations WHERE email=? OR (username=? AND expires_at <= NOW())")->execute([$email, $username]);
// Resolve referral code to user ID
$referrerId = null;
if ($referralCode) {
$refStmt = db()->prepare("SELECT id FROM users WHERE referral_code=? AND status='active'");
$refStmt->execute([strtoupper(trim($referralCode))]);
$refUser = $refStmt->fetch();
if ($refUser) $referrerId = (int)$refUser['id'];
}
$token = bin2hex(random_bytes(32));
$hash = password_hash($password, PASSWORD_BCRYPT);
$expiresAt = date('Y-m-d H:i:s', time() + VERIFY_TTL);
$stmt = db()->prepare("INSERT INTO pending_registrations (username, password, alias, email, token, referred_by, expires_at) VALUES (?,?,?,?,?,?,?)");
$stmt->execute([$username, $hash, $alias, $email, $token, $referrerId, $expiresAt]);
$sent = sendVerificationEmail($email, $alias, $token);
if (!$sent) {
// Email failed but keep registration — user can resend from login screen
error_log('[TomTomGames] Verification email failed for ' . $email);
return ['success' => true, 'email' => $email, 'mail_warning' => true];
}
return ['success' => true, 'email' => $email];
}
/**
* Consume a verification token: create the real user, delete pending row.
*/
function verifyEmailToken(string $token): array {
$token = trim($token);
if (empty($token)) return ['success' => false, 'error' => 'Invalid verification link.'];
$stmt = db()->prepare("SELECT * FROM pending_registrations WHERE token=? AND expires_at > NOW()");
$stmt->execute([$token]);
$pending = $stmt->fetch();
if (!$pending) {
return ['success' => false, 'error' => 'This verification link is invalid or has expired. Please register again.'];
}
// Check username/email not taken since pending was created
$s = db()->prepare("SELECT id FROM users WHERE username=? OR email=?");
$s->execute([$pending['username'], $pending['email']]);
if ($s->fetch()) {
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
return ['success' => false, 'error' => 'This username or email was already registered. Please log in.'];
}
db()->beginTransaction();
try {
// Create the user
$ins = db()->prepare("INSERT INTO users (username, password, alias, email, email_verified, status) VALUES (?,?,?,?,1,'active')");
$ins->execute([$pending['username'], $pending['password'], $pending['alias'], $pending['email']]);
$userId = db()->lastInsertId();
// Generate unique referral code
$code = strtoupper(substr(md5($userId . uniqid()), 0, 8));
db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code, $userId]);
// Track referral if referred_by is in pending
if (!empty($pending['referred_by'])) {
$referrerId = (int)$pending['referred_by'];
try {
db()->prepare("INSERT IGNORE INTO referrals (referrer_id, referred_id, status) VALUES (?,?,'pending')")
->execute([$referrerId, $userId]);
db()->prepare("UPDATE users SET referred_by=? WHERE id=?")->execute([$referrerId, $userId]);
} catch(Exception $e) {}
}
// Delete pending row
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
return ['success' => false, 'error' => 'Account creation failed. Please try again.'];
}
// Auto-login
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $pending['username'];
$_SESSION['alias'] = $pending['alias'];
$_SESSION['is_admin'] = 0;
return ['success' => true, 'username' => $pending['username'], 'alias' => $pending['alias']];
}
/**
* Resend verification email for an unverified account.
*/
function resendVerification(string $email): array {
$email = strtolower(trim($email));
$stmt = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW() ORDER BY id DESC LIMIT 1");
$stmt->execute([$email]);
$pending = $stmt->fetch();
if (!$pending) {
return ['success' => false, 'error' => 'No pending registration found for that email, or it has expired. Please register again.'];
}
$alias = db()->prepare("SELECT alias FROM pending_registrations WHERE id=?");
$alias->execute([$pending['id']]);
$row = $alias->fetch();
sendVerificationEmail($email, $row['alias'] ?? 'Player', $pending['token']);
return ['success' => true];
}
function logoutUser(): void {
$_SESSION = [];
session_destroy();
}
// ─── Comprehensive Audit Logger ────────────────────────────
function logActivity(
string $action,
?int $userId = null,
?int $adminId = null,
string $entityType= '',
int $entityId = 0,
string $detail = '',
string $ip = '',
string $category = 'general',
string $oldValue = '',
string $newValue = '',
string $severity = 'info'
): void {
try {
// Auto-purge entries older than 90 days (probabilistic — 3% of calls)
if (rand(1, 100) <= 3) {
db()->exec("DELETE FROM activity_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)");
}
$ip = $ip ?: ($_SERVER['REMOTE_ADDR'] ?? '');
$userAgent = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 300);
$page = substr(($_SERVER['REQUEST_URI'] ?? ''), 0, 200);
$sessionId = session_id() ?: '';
db()->prepare("
INSERT INTO activity_log
(user_id, admin_id, action, category, entity_type, entity_id, detail,
old_value, new_value, ip, user_agent, page, session_id, severity)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
")->execute([
$userId ?: null,
$adminId ?: null,
substr($action, 0, 120),
$category,
$entityType,
$entityId ?: null,
substr($detail, 0, 2000),
substr($oldValue, 0, 2000),
substr($newValue, 0, 2000),
$ip,
$userAgent,
$page,
$sessionId,
$severity,
]);
} catch (Exception $e) { /* never fail silently */ }
}
// Convenience wrappers
function logPlayerAction(string $action, int $userId, string $detail='', string $category='player', string $severity='info'): void {
logActivity($action, $userId, null, 'user', $userId, $detail, '', $category, '', '', $severity);
}
function logAdminAction(string $action, int $adminId, string $entityType='', int $entityId=0, string $detail='', string $old='', string $new='', string $severity='info'): void {
logActivity($action, null, $adminId, $entityType, $entityId, $detail, '', 'admin', $old, $new, $severity);
}
function logSecurityEvent(string $action, ?int $userId=null, string $detail='', string $severity='warning'): void {
logActivity($action, $userId, null, 'security', 0, $detail, '', 'security', '', '', $severity);
}
// ─── App Version ───────────────────────────────────────────
function getAppVersion(): string {
try {
$v = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn();
return $v ?: '1.0.0';
} catch(Exception $e) { return '1.0.0'; }
}
+541
View File
@@ -0,0 +1,541 @@
<?php
require_once __DIR__ . '/config.php';
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
try {
$this->pdo = new PDO(
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4",
DB_USER, DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['success'=>false,'error'=>'Database connection failed.']));
}
}
public static function getInstance(): self {
if (!self::$instance) self::$instance = new self();
return self::$instance;
}
public function getConnection(): PDO { return $this->pdo; }
}
function db(): PDO { return Database::getInstance()->getConnection(); }
// Check if a column exists — MySQL 5.x compatible
function colExists(PDO $pdo, string $table, string $col): bool {
return (bool)$pdo->query("SHOW COLUMNS FROM `$table` LIKE '$col'")->fetch();
}
function initDB(): void {
$pdo = db();
// Each table in its own exec() — PDO only runs one statement per call
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
alias VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE,
email_verified TINYINT(1) DEFAULT 0,
tokens DECIMAL(10,2) DEFAULT 0,
is_admin TINYINT(1) DEFAULT 0,
status ENUM('active','suspended') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS pending_registrations (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
alias VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL,
token VARCHAR(64) UNIQUE NOT NULL,
referred_by INT DEFAULT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS token_purchases (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
tokens INT NOT NULL,
amount_cents INT NOT NULL,
payment_method VARCHAR(20) DEFAULT 'card',
square_payment_id VARCHAR(255),
platform_id VARCHAR(50),
game_alias VARCHAR(100),
player_name VARCHAR(100),
billing_name VARCHAR(160),
billing_address VARCHAR(200),
billing_city VARCHAR(80),
billing_state VARCHAR(2),
billing_zip VARCHAR(10),
billing_email VARCHAR(150),
is_custom TINYINT(1) DEFAULT 0,
failure_reason TEXT,
card_brand VARCHAR(30),
card_last4 VARCHAR(4),
receipt_url VARCHAR(512),
status ENUM('pending','completed','failed') DEFAULT 'pending',
admin_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS cashout_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform_id VARCHAR(50) NOT NULL,
alias VARCHAR(100) NOT NULL,
tokens DECIMAL(10,2) NOT NULL,
status ENUM('pending','approved','rejected') DEFAULT 'pending',
admin_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS platform_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform_slug VARCHAR(50) NOT NULL,
requested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
status ENUM('pending','approved','denied','deleted') DEFAULT 'pending',
platform_username VARCHAR(100),
platform_password VARCHAR(200),
admin_note VARCHAR(300),
resolved_at DATETIME,
admin_id INT,
UNIQUE KEY uq_user_platform (user_id, platform_slug),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS admin_payout_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
method_key VARCHAR(50) UNIQUE NOT NULL,
label VARCHAR(100) NOT NULL,
method_type ENUM('manual','square_gift_card') DEFAULT 'manual',
is_enabled TINYINT(1) DEFAULT 1,
handle VARCHAR(200),
instructions TEXT,
sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS cashout_transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
cashout_id INT NOT NULL,
admin_id INT NOT NULL,
payout_method VARCHAR(50) NOT NULL,
payout_type ENUM('manual','square_gift_card') DEFAULT 'manual',
amount_cents INT NOT NULL,
square_txn_id VARCHAR(200),
gift_card_gan VARCHAR(100),
gift_card_balance INT,
note TEXT,
status ENUM('pending','completed','failed') DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (cashout_id) REFERENCES cashout_requests(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS activity_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
admin_id INT,
action VARCHAR(120) NOT NULL,
category VARCHAR(40) DEFAULT 'general',
entity_type VARCHAR(40),
entity_id INT,
detail TEXT,
old_value TEXT,
new_value TEXT,
ip VARCHAR(45),
user_agent VARCHAR(300),
page VARCHAR(200),
session_id VARCHAR(64),
severity ENUM('info','warning','critical') DEFAULT 'info',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created (created_at),
INDEX idx_user (user_id),
INDEX idx_admin (admin_id),
INDEX idx_category (category),
INDEX idx_severity (severity)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS broadcasts (
id INT AUTO_INCREMENT PRIMARY KEY,
admin_id INT NOT NULL,
subject VARCHAR(200) NOT NULL,
message TEXT NOT NULL,
target ENUM('all','verified','unverified','admins') DEFAULT 'all',
sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (admin_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS broadcast_reads (
id INT AUTO_INCREMENT PRIMARY KEY,
broadcast_id INT NOT NULL,
user_id INT NOT NULL,
read_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_br (broadcast_id, user_id),
FOREIGN KEY (broadcast_id) REFERENCES broadcasts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS broadcast_replies (
id INT AUTO_INCREMENT PRIMARY KEY,
broadcast_id INT NOT NULL,
user_id INT NOT NULL,
message TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (broadcast_id) REFERENCES broadcasts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS cashout_method_types (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(50) UNIQUE NOT NULL,
label VARCHAR(100) NOT NULL,
icon VARCHAR(10) DEFAULT '💰',
description VARCHAR(200),
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS payout_methods (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
method_type VARCHAR(50) NOT NULL,
label VARCHAR(100) NOT NULL,
account_handle VARCHAR(200) NOT NULL,
is_default TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Add payout fields to cashout_requests if not present
try {
$cols = array_column($pdo->query("SHOW COLUMNS FROM cashout_requests")->fetchAll(), 'Field');
if (!in_array('payout_method_type', $cols)) {
$pdo->exec("ALTER TABLE cashout_requests ADD COLUMN payout_method_type VARCHAR(50) AFTER alias");
$pdo->exec("ALTER TABLE cashout_requests ADD COLUMN payout_handle VARCHAR(200) AFTER payout_method_type");
}
if (!in_array('sent_note', $cols)) {
$pdo->exec("ALTER TABLE cashout_requests ADD COLUMN sent_note TEXT AFTER admin_note");
}
$pdo->exec("ALTER TABLE cashout_requests MODIFY COLUMN status ENUM('pending','approved','sent','rejected','deleted') DEFAULT 'pending'");
} catch (Exception $e) { /* ignore — columns may already exist */ }
$pdo->exec("CREATE TABLE IF NOT EXISTS saved_billing (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT UNIQUE NOT NULL,
first_name VARCHAR(80),
last_name VARCHAR(80),
email VARCHAR(150),
address VARCHAR(200),
city VARCHAR(80),
state VARCHAR(2),
zip VARCHAR(10),
card_brand VARCHAR(30),
card_last4 VARCHAR(4),
card_exp_month VARCHAR(2),
card_exp_year VARCHAR(4),
sq_card_id VARCHAR(255),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS platforms (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
player_url VARCHAR(500) NOT NULL,
console_url VARCHAR(500),
color VARCHAR(20) DEFAULT '#f0c040',
icon_path VARCHAR(200),
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS payment_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
method_key VARCHAR(50) UNIQUE NOT NULL,
label VARCHAR(100) NOT NULL,
is_enabled TINYINT(1) DEFAULT 1,
handle VARCHAR(200),
instructions TEXT,
sort_order INT DEFAULT 0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS game_aliases (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform_slug VARCHAR(50) NOT NULL,
alias VARCHAR(100) NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_platform (user_id, platform_slug),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Track whether user has seen the post-verify onboarding
try {
if (!colExists($pdo,'users','onboarding_done')) {
$pdo->exec("ALTER TABLE users ADD COLUMN onboarding_done TINYINT(1) DEFAULT 0");
}
} catch (Exception $e) {}
$pdo->exec("CREATE TABLE IF NOT EXISTS chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
sender ENUM('user','admin') NOT NULL,
message TEXT NOT NULL,
is_read TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Add missing columns to existing tables — MySQL 5.x compatible (no IF NOT EXISTS)
$addCols = [
['token_purchases', 'billing_name', "VARCHAR(160)", 'player_name'],
['token_purchases', 'billing_address', "VARCHAR(200)", 'billing_name'],
['token_purchases', 'billing_city', "VARCHAR(80)", 'billing_address'],
['token_purchases', 'billing_state', "VARCHAR(2)", 'billing_city'],
['token_purchases', 'billing_zip', "VARCHAR(10)", 'billing_state'],
['token_purchases', 'billing_email', "VARCHAR(150)", 'billing_zip'],
['token_purchases', 'is_custom', "TINYINT(1) DEFAULT 0", 'billing_email'],
['token_purchases', 'failure_reason', "TEXT", 'is_custom'],
['token_purchases', 'card_brand', "VARCHAR(30)", 'failure_reason'],
['token_purchases', 'card_last4', "VARCHAR(4)", 'card_brand'],
['token_purchases', 'receipt_url', "VARCHAR(512)", 'card_last4'],
['token_purchases', 'admin_note', "TEXT", 'status'],
['users', 'email_verified', "TINYINT(1) DEFAULT 0", 'email'],
];
foreach ($addCols as [$tbl, $col, $def, $after]) {
if (!colExists($pdo, $tbl, $col)) {
try {
$pdo->exec("ALTER TABLE `$tbl` ADD COLUMN `$col` $def AFTER `$after`");
} catch (Exception $e) { /* ignore — concurrent init */ }
}
}
}
try { initDB(); } catch (Exception $e) { /* already initialised */ }
// Seed admin_payout_settings if empty
try {
if (db()->query("SELECT COUNT(*) FROM admin_payout_settings")->fetchColumn() == 0) {
$seeds = [
['venmo', 'Venmo', 'manual', 1, '@your-venmo', 'Send via Venmo app'],
['cashapp', 'Cash App', 'manual', 1, '$yourcashtag', 'Send via Cash App'],
['zelle', 'Zelle', 'manual', 1, 'your@email', 'Send via Zelle'],
['chime', 'Chime', 'manual', 1, '', 'Send via Chime'],
['square_gift', 'Square Gift Card','square_gift_card',1, '', 'Instant Square gift card — player redeems anywhere Square is accepted'],
];
$st = db()->prepare("INSERT IGNORE INTO admin_payout_settings (method_key,label,method_type,is_enabled,handle,instructions,sort_order) VALUES (?,?,?,?,?,?,?)");
foreach ($seeds as $i => $s) { $st->execute([$s[0],$s[1],$s[2],$s[3],$s[4],$s[5],$i]); }
}
} catch(Exception $e) {}
// ─── REFERRAL TABLES ──────────────────────────────────────
try {
db()->exec("CREATE TABLE IF NOT EXISTS referral_tiers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
min_referrals INT NOT NULL DEFAULT 1,
tokens_per_ref DECIMAL(10,2) NOT NULL DEFAULT 10,
bonus_tokens DECIMAL(10,2) NOT NULL DEFAULT 0,
description VARCHAR(300),
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
db()->exec("CREATE TABLE IF NOT EXISTS referrals (
id INT AUTO_INCREMENT PRIMARY KEY,
referrer_id INT NOT NULL,
referred_id INT NOT NULL UNIQUE,
tier_id INT,
status ENUM('pending','verified','denied','deleted') DEFAULT 'pending',
tokens_awarded DECIMAL(10,2) DEFAULT 0,
admin_id INT,
admin_note VARCHAR(300),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME,
FOREIGN KEY (referrer_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (referred_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
db()->exec("CREATE TABLE IF NOT EXISTS referral_social_shares (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform VARCHAR(50) NOT NULL,
bonus_tokens DECIMAL(10,2) DEFAULT 0,
status ENUM('pending','approved','denied') DEFAULT 'pending',
admin_id INT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch(Exception $e) {}
// Seed default referral tiers
try {
if (db()->query("SELECT COUNT(*) FROM referral_tiers")->fetchColumn() == 0) {
$seeds = [
['Bronze Referrer', 1, 5, 0, 'Earn 5 tokens for each verified referral', 1, 0],
['Silver Referrer', 5, 8, 25, 'Earn 8 tokens per referral + 25 bonus at 5 referrals', 1, 1],
['Gold Referrer', 10, 10, 100,'Earn 10 tokens per referral + 100 bonus at 10 referrals',1, 2],
['Elite Referrer', 25, 15, 250,'Earn 15 tokens per referral + 250 bonus at 25 referrals',1, 3],
];
$st = db()->prepare("INSERT INTO referral_tiers (name,min_referrals,tokens_per_ref,bonus_tokens,description,is_active,sort_order) VALUES (?,?,?,?,?,?,?)");
foreach ($seeds as $s) $st->execute($s);
}
} catch(Exception $e) {}
// Add referral_code to users if missing
try {
$cols = array_column(db()->query("SHOW COLUMNS FROM users")->fetchAll(), 'Field');
if (!in_array('referral_code', $cols)) {
db()->exec("ALTER TABLE users ADD COLUMN referral_code VARCHAR(20) UNIQUE");
// Generate codes for existing users
$users = db()->query("SELECT id FROM users WHERE referral_code IS NULL")->fetchAll();
$upd = db()->prepare("UPDATE users SET referral_code=? WHERE id=?");
foreach ($users as $u) {
$code = strtoupper(substr(md5($u['id'].uniqid()),0,8));
$upd->execute([$code, $u['id']]);
}
}
} catch(Exception $e) {}
// Add referred_by to users if missing
try {
$cols = array_column(db()->query("SHOW COLUMNS FROM users")->fetchAll(), 'Field');
if (!in_array('referred_by', $cols)) {
db()->exec("ALTER TABLE users ADD COLUMN referred_by INT DEFAULT NULL");
}
} catch(Exception $e) {}
// Add card payment_settings row if missing
try {
$cardCount = db()->query("SELECT COUNT(*) FROM payment_settings WHERE method_key='card'")->fetchColumn();
if ($cardCount == 0) {
db()->exec("INSERT IGNORE INTO payment_settings (method_key,label,handle,instructions,is_enabled,sort_order) VALUES ('card','Credit / Debit Card','','Processed via Square',1,-1)");
}
} catch(Exception $e) {}
// Expand activity_log if columns missing
try {
$alCols = array_column(db()->query("SHOW COLUMNS FROM activity_log")->fetchAll(), 'Field');
$alAdd = [
'category' => "ALTER TABLE activity_log ADD COLUMN category VARCHAR(40) DEFAULT 'general' AFTER action",
'old_value' => "ALTER TABLE activity_log ADD COLUMN old_value TEXT AFTER detail",
'new_value' => "ALTER TABLE activity_log ADD COLUMN new_value TEXT AFTER old_value",
'user_agent' => "ALTER TABLE activity_log ADD COLUMN user_agent VARCHAR(300) AFTER ip",
'page' => "ALTER TABLE activity_log ADD COLUMN page VARCHAR(200) AFTER user_agent",
'session_id' => "ALTER TABLE activity_log ADD COLUMN session_id VARCHAR(64) AFTER page",
'severity' => "ALTER TABLE activity_log ADD COLUMN severity ENUM('info','warning','critical') DEFAULT 'info' AFTER session_id",
];
foreach ($alAdd as $col => $sql) {
if (!in_array($col, $alCols)) db()->exec($sql);
}
// Change detail to TEXT if it's VARCHAR
$detailType = '';
foreach (db()->query("SHOW COLUMNS FROM activity_log")->fetchAll() as $col) {
if ($col['Field'] === 'detail') $detailType = $col['Type'];
}
if (stripos($detailType, 'varchar') !== false) {
db()->exec("ALTER TABLE activity_log MODIFY COLUMN detail TEXT");
}
} catch(Exception $e) {}
// Add platform_onboarding_done to users if not exists
try {
$cols = array_column(db()->query("SHOW COLUMNS FROM users")->fetchAll(), 'Field');
if (!in_array('platform_onboarding_done', $cols)) {
db()->exec("ALTER TABLE users ADD COLUMN platform_onboarding_done TINYINT(1) DEFAULT 0");
}
} catch(Exception $e){}
// App version table
try {
db()->exec("CREATE TABLE IF NOT EXISTS app_version (
id INT AUTO_INCREMENT PRIMARY KEY,
version VARCHAR(20) NOT NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Seed initial version if empty
if (db()->query("SELECT COUNT(*) FROM app_version")->fetchColumn() == 0) {
db()->exec("INSERT INTO app_version (version, notes) VALUES ('1.0.0', 'Initial release')");
}
} catch(Exception $e) {}
// Seed cashout_method_types if empty
try {
$cmtCount = db()->query("SELECT COUNT(*) FROM cashout_method_types")->fetchColumn();
if ($cmtCount == 0) {
$types = [
['venmo', 'Venmo', '💙', 'Send via Venmo username', 1, 0],
['cashapp', 'Cash App', '💚', 'Send via Cash App $cashtag', 1, 1],
['zelle', 'Zelle', '💜', 'Send via Zelle phone or email', 1, 2],
['chime', 'Chime', '🟢', 'Send via Chime account', 1, 3],
['bank', 'Bank Transfer', '🏦', 'Direct bank transfer', 1, 4],
['other', 'Other', '💰', 'Other payment method', 1, 5],
];
$stmt = db()->prepare("INSERT IGNORE INTO cashout_method_types (slug,label,icon,description,is_active,sort_order) VALUES (?,?,?,?,?,?)");
foreach ($types as $t) $stmt->execute($t);
}
} catch (Exception $e) { /* ignore */ }
// Seed payment_settings from config if empty
try {
$pmtCount = db()->query("SELECT COUNT(*) FROM payment_settings")->fetchColumn();
if ($pmtCount == 0) {
$methods = [
['card', 'Credit / Debit Card', '', 'Processed via Square', 1, -1],
['venmo', 'Venmo', PAY_VENMO, 'Send payment via Venmo', 1, 0],
['chime', 'Chime', PAY_CHIME, 'Send payment via Chime', 1, 1],
['cashapp', 'Cash App', PAY_CASHAPP, 'Send payment via Cash App',1, 2],
['zelle', 'Zelle', PAY_ZELLE, 'Send payment via Zelle', 1, 3],
];
$stmt = db()->prepare("INSERT IGNORE INTO payment_settings (method_key,label,handle,instructions,is_enabled,sort_order) VALUES (?,?,?,?,1,?)");
foreach ($methods as $m) {
$stmt->execute([$m[0],$m[1],$m[2],$m[3],$m[5]]);
}
}
} catch (Exception $e) { /* ignore */ }
// Seed platforms table from config if empty
try {
$count = db()->query("SELECT COUNT(*) FROM platforms")->fetchColumn();
if ($count == 0) {
$platforms = json_decode(PLATFORMS, true);
$stmt = db()->prepare("INSERT IGNORE INTO platforms (slug,name,player_url,color,sort_order) VALUES (?,?,?,?,?)");
foreach ($platforms as $i => $p) {
$stmt->execute([$p['id'], $p['name'], $p['url'], $p['color'], $i]);
}
}
} catch (Exception $e) { /* ignore */ }
// Always ensure admin accounts are email-verified
try {
if (colExists(db(), 'users', 'email_verified')) {
db()->exec("UPDATE users SET email_verified=1 WHERE is_admin=1 AND email_verified=0");
}
} catch (Exception $e) { /* ignore */ }
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* TomTomGames Mailer — SendGrid HTTP API (cURL)
*/
function sendVerificationEmail(string $toEmail, string $toName, string $token): bool {
$siteName = defined('SITE_NAME') ? SITE_NAME : 'TomTomGames';
$siteUrl = defined('SITE_URL') ? SITE_URL : 'https://tomtomgames.com';
$verifyUrl = $siteUrl . '/verify.php?token=' . urlencode($token);
$subject = "Verify your {$siteName} account";
$text = "Welcome to {$siteName}, {$toName}!\n\n"
. "Verify your email address:\n{$verifyUrl}\n\n"
. "This link expires in 24 hours.\n\n"
. "If you did not create this account, ignore this email.\n\n"
. "— The {$siteName} Team";
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head>'
. '<body style="margin:0;padding:0;background:#0a0a12;font-family:Arial,sans-serif">'
. '<table width="100%" cellpadding="0" cellspacing="0" style="background:#0a0a12;padding:32px 16px">'
. '<tr><td align="center"><table width="520" cellpadding="0" cellspacing="0" '
. 'style="background:#1a1a2e;border:1px solid rgba(255,255,255,.08);border-radius:16px;overflow:hidden;max-width:520px;width:100%">'
. '<tr><td style="background:linear-gradient(135deg,#f0c040,#ff6b35);padding:28px 32px;text-align:center">'
. '<span style="font-weight:900;font-size:24px;color:#000">&#127918; ' . htmlspecialchars($siteName) . '</span>'
. '</td></tr>'
. '<tr><td style="padding:36px 32px;color:#e8e8f0">'
. '<h2 style="margin:0 0 16px;font-size:22px;color:#f0c040">Verify your account</h2>'
. '<p style="margin:0 0 12px;font-size:15px;color:#aaaacc;line-height:1.6">Hey <strong style="color:#e8e8f0">'
. htmlspecialchars($toName) . '</strong>,</p>'
. '<p style="margin:0 0 24px;font-size:15px;color:#aaaacc;line-height:1.6">Thanks for signing up! Click below to verify your email and activate your account.</p>'
. '<div style="text-align:center;margin-bottom:28px">'
. '<a href="' . htmlspecialchars($verifyUrl) . '" style="display:inline-block;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-weight:700;font-size:16px;padding:16px 40px;border-radius:10px;text-decoration:none;letter-spacing:.5px">VERIFY MY ACCOUNT</a>'
. '</div>'
. '<p style="font-size:12px;color:#666688;margin-bottom:8px">Or paste this into your browser:</p>'
. '<p style="font-size:12px;color:#00e5ff;word-break:break-all;margin:0 0 24px">' . htmlspecialchars($verifyUrl) . '</p>'
. '<p style="font-size:12px;color:#555577;border-top:1px solid rgba(255,255,255,.06);padding-top:16px;margin:0">'
. 'Link expires in 24 hours. Did not sign up? You can safely ignore this email.</p>'
. '</td></tr>'
. '<tr><td style="background:#111122;padding:16px 32px;text-align:center">'
. '<span style="font-size:11px;color:#444466">&copy; ' . htmlspecialchars($siteName)
. ' &middot; <a href="' . htmlspecialchars($siteUrl) . '" style="color:#f0c040;text-decoration:none">'
. htmlspecialchars($siteUrl) . '</a></span>'
. '</td></tr></table></td></tr></table></body></html>';
return sendgridSend($toEmail, $toName, $subject, $text, $html);
}
function sendgridSend(string $toEmail, string $toName, string $subject, string $textBody, string $htmlBody = ''): bool {
$apiKey = defined('SENDGRID_API_KEY') ? SENDGRID_API_KEY : '';
if (!$apiKey) {
error_log('[TomTomGames mailer] SENDGRID_API_KEY not defined');
return false;
}
$payload = json_encode([
'personalizations' => [['to' => [['email' => $toEmail, 'name' => $toName]]]],
'from' => [
'email' => defined('SMTP_FROM') ? SMTP_FROM : 'noreply@tomtomgames.com',
'name' => defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'TomTomGames',
],
'subject' => $subject,
'content' => array_values(array_filter([
['type' => 'text/plain', 'value' => $textBody],
$htmlBody ? ['type' => 'text/html', 'value' => $htmlBody] : null,
])),
]);
$ch = curl_init('https://api.sendgrid.com/v3/mail/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($httpCode === 202) return true;
error_log('[TomTomGames mailer] SendGrid HTTP ' . $httpCode . ' err=' . $curlErr . ' body=' . $response);
return false;
}
+183
View File
@@ -0,0 +1,183 @@
<?php
/**
* TomTomGames SMTP Mailer
* Sends email via SMTP using PHP's socket functions.
* Compatible with Outlook/Office365, Gmail, and standard SMTP servers.
*/
class SmtpMailer {
private string $host;
private int $port;
private string $user;
private string $pass;
private string $fromEmail;
private string $fromName;
private bool $debug;
private array $log = [];
public function __construct(
string $host,
int $port,
string $user,
string $pass,
string $fromEmail,
string $fromName,
bool $debug = false
) {
$this->host = $host;
$this->port = $port;
$this->user = $user;
$this->pass = $pass;
$this->fromEmail = $fromEmail;
$this->fromName = $fromName;
$this->debug = $debug;
}
public function send(string $toEmail, string $toName, string $subject, string $textBody, string $htmlBody = ''): bool {
$errno = 0; $errstr = '';
$socket = fsockopen("tcp://{$this->host}", $this->port, $errno, $errstr, 15);
if (!$socket) { $this->log[] = "Connect failed: $errstr ($errno)"; return false; }
stream_set_timeout($socket, 15);
try {
// Read greeting
$this->expect($socket, 220, "greeting");
// EHLO
$this->send_cmd($socket, "EHLO " . gethostname());
$ehlo = $this->read_response($socket);
if (substr($ehlo, 0, 3) !== '250') { throw new Exception("EHLO failed: $ehlo"); }
// STARTTLS
$this->send_cmd($socket, "STARTTLS");
$this->expect($socket, 220, "STARTTLS");
// Upgrade to TLS
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
throw new Exception("TLS upgrade failed");
}
// EHLO again after TLS
$this->send_cmd($socket, "EHLO " . gethostname());
$ehlo2 = $this->read_response($socket);
if (substr($ehlo2, 0, 3) !== '250') { throw new Exception("EHLO2 failed: $ehlo2"); }
// AUTH LOGIN
$this->send_cmd($socket, "AUTH LOGIN");
$this->expect($socket, 334, "AUTH LOGIN prompt");
$this->send_cmd($socket, base64_encode($this->user));
$this->expect($socket, 334, "Username prompt");
$this->send_cmd($socket, base64_encode($this->pass));
$this->expect($socket, 235, "AUTH success");
// MAIL FROM
$this->send_cmd($socket, "MAIL FROM:<{$this->fromEmail}>");
$this->expect($socket, 250, "MAIL FROM");
// RCPT TO
$this->send_cmd($socket, "RCPT TO:<{$toEmail}>");
$this->expect($socket, 250, "RCPT TO");
// DATA
$this->send_cmd($socket, "DATA");
$this->expect($socket, 354, "DATA");
// Build message
$boundary = 'boundary_' . md5(uniqid());
$fromHdr = $this->encodeName($this->fromName) . " <{$this->fromEmail}>";
$toHdr = $this->encodeName($toName) . " <{$toEmail}>";
$subjHdr = $this->encodeSubject($subject);
$msgId = '<' . time() . '.' . rand(1000,9999) . '@' . $this->host . '>';
$headers = "From: $fromHdr\r\n";
$headers .= "To: $toHdr\r\n";
$headers .= "Subject: $subjHdr\r\n";
$headers .= "Message-ID: $msgId\r\n";
$headers .= "Date: " . date('r') . "\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "X-Mailer: TomTomGames/1.0\r\n";
if ($htmlBody) {
$headers .= "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n";
$body = "--$boundary\r\n";
$body .= "Content-Type: text/plain; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
$body .= quoted_printable_encode($textBody) . "\r\n";
$body .= "--$boundary\r\n";
$body .= "Content-Type: text/html; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
$body .= quoted_printable_encode($htmlBody) . "\r\n";
$body .= "--$boundary--";
} else {
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
$body = quoted_printable_encode($textBody);
}
// Dot-stuff and send
$message = $headers . "\r\n" . $body;
$message = preg_replace('/^\./m', '..', $message);
fwrite($socket, $message . "\r\n.\r\n");
$this->expect($socket, 250, "Message accepted");
// QUIT
$this->send_cmd($socket, "QUIT");
fclose($socket);
return true;
} catch (Exception $e) {
$this->log[] = "Error: " . $e->getMessage();
try { $this->send_cmd($socket, "QUIT"); } catch(Exception $_) {}
fclose($socket);
return false;
}
}
private function send_cmd($socket, string $cmd): void {
if ($this->debug) $this->log[] = ">>> $cmd";
fwrite($socket, $cmd . "\r\n");
}
private function read_response($socket): string {
$response = '';
while ($line = fgets($socket, 512)) {
if ($this->debug) $this->log[] = "<<< " . trim($line);
$response .= $line;
if (isset($line[3]) && $line[3] === ' ') break; // Multi-line ends when 4th char is space
}
return trim($response);
}
private function expect($socket, int $code, string $context): void {
$resp = $this->read_response($socket);
if (substr($resp, 0, 3) !== (string)$code) {
throw new Exception("Expected $code at $context, got: $resp");
}
}
private function encodeName(string $name): string {
if (preg_match('/[^\x20-\x7E]/', $name) || strpbrk($name, '"<>()')) {
return '=?UTF-8?B?' . base64_encode($name) . '?=';
}
return '"' . addslashes($name) . '"';
}
private function encodeSubject(string $subject): string {
if (preg_match('/[^\x20-\x7E]/', $subject)) {
return '=?UTF-8?B?' . base64_encode($subject) . '?=';
}
return $subject;
}
public function getLog(): array { return $this->log; }
}
/**
* Factory — returns a ready-to-use mailer using config constants.
*/
function mailer(): SmtpMailer {
return new SmtpMailer(
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS,
SMTP_FROM, SMTP_FROM_NAME
);
}
+115
View File
@@ -0,0 +1,115 @@
<?php
require_once __DIR__ . '/config.php';
class SquarePayment {
private string $baseUrl;
private string $token;
public function __construct() {
$this->token = SQUARE_ACCESS_TOKEN;
$this->baseUrl = SQUARE_ENV === 'production'
? 'https://connect.squareup.com/v2'
: 'https://connect.squareupsandbox.com/v2';
}
public function charge(
string $sourceId,
int $amountCents,
string $note = '',
string $cardholderName= '',
array $billingAddress= [],
string $buyerEmail = ''
): array {
$body = [
'idempotency_key' => uniqid('tg_', true),
'source_id' => $sourceId,
'amount_money' => ['amount' => $amountCents, 'currency' => 'USD'],
'location_id' => SQUARE_LOCATION_ID,
'note' => $note ?: 'TomGames Token Purchase',
'autocomplete' => true,
];
if ($cardholderName) {
$body['buyer_email_address'] = $buyerEmail ?: null;
}
if (!empty($billingAddress)) {
$body['billing_address'] = array_filter($billingAddress);
}
if ($buyerEmail && filter_var($buyerEmail, FILTER_VALIDATE_EMAIL)) {
$body['buyer_email_address'] = $buyerEmail;
$body['receipt_email'] = $buyerEmail;
}
$ch = curl_init($this->baseUrl . '/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->token,
'Square-Version: 2024-01-18',
],
CURLOPT_POSTFIELDS => json_encode(array_filter($body, fn($v) => $v !== null)),
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($curlErr) return ['success'=>false,'error'=>'Connection error. Please try again.'];
$data = json_decode($response, true);
if ($httpCode === 200 && isset($data['payment']['id'])) {
return [
'success' => true,
'payment_id' => $data['payment']['id'],
'status' => $data['payment']['status'],
'receipt_url'=> $data['payment']['receipt_url'] ?? null,
'card_brand' => $data['payment']['card_details']['card']['card_brand'] ?? null,
'last_4' => $data['payment']['card_details']['card']['last_4'] ?? null,
];
}
$errorMsg = $data['errors'][0]['detail'] ?? ($data['errors'][0]['code'] ?? 'Payment failed. Please try again.');
return ['success'=>false,'error'=>$errorMsg];
}
public static function sdkUrl(): string {
return SQUARE_ENV === 'production'
? 'https://web.squarecdn.com/v1/square.js'
: 'https://sandbox.web.squarecdn.com/v1/square.js';
}
// Generic POST for Square APIs (gift cards, etc.)
public static function post(string $path, array $body): array {
$baseUrl = SQUARE_ENV === 'production'
? 'https://connect.squareup.com'
: 'https://connect.squareupsandbox.com';
$url = $baseUrl . $path;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Square-Version: 2024-01-18',
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
],
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
$httpCode= curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) throw new Exception('Square connection error: ' . $err);
$data = json_decode($resp, true);
if (isset($data['errors'])) {
throw new Exception($data['errors'][0]['detail'] ?? 'Square API error');
}
return $data;
}
}