mirror of
https://github.com/myronblair/tomtomgames-app
synced 2026-06-30 17:49:57 -05:00
v1.0.0 - Initial release: registration, SendGrid email, Square payments, cashout, admin panel
This commit is contained in:
@@ -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 3–50 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
@@ -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 */ }
|
||||
@@ -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">🎮 ' . 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">© ' . htmlspecialchars($siteName)
|
||||
. ' · <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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user