mirror of
https://github.com/myronblair/tomtomgames-app
synced 2026-06-30 17:49:57 -05:00
294 lines
12 KiB
PHP
294 lines
12 KiB
PHP
<?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'; }
|
||
}
|