mirror of
https://github.com/myronblair/tomtomgames-app
synced 2026-06-30 17:49:57 -05:00
v1.0.4 - Clean start
This commit is contained in:
+73
-157
@@ -6,16 +6,31 @@ function isLoggedIn(): bool {
|
||||
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
function requireLogin(): void {
|
||||
if (!isLoggedIn()) {
|
||||
header('Location: /'); exit;
|
||||
|
||||
function logoutUser(): void {
|
||||
// Clear all session data
|
||||
$_SESSION = [];
|
||||
// Destroy session cookie
|
||||
if (isset($_COOKIE[session_name()])) {
|
||||
setcookie(session_name(), '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
function requireLogin(): void {
|
||||
if (!isLoggedIn()) { header('Location: /'); exit; }
|
||||
}
|
||||
|
||||
function requireAdmin(): void {
|
||||
requireLogin();
|
||||
if (empty($_SESSION['is_admin'])) {
|
||||
header('Location: /'); exit;
|
||||
header('Location: /admin/login.php'); exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,262 +47,163 @@ function loginUser(string $username, string $password): array {
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !password_verify($password, $user['password'])) {
|
||||
logActivity('LOGIN_FAILED', null, null, 'auth', 0, 'Failed login: '.$username, '', 'security', '', '', 'warning');
|
||||
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'],
|
||||
];
|
||||
return ['success'=>false,'error'=>'Please verify your email address before logging in.','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_regenerate_id(true);
|
||||
$_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']]);
|
||||
logActivity('LOGIN_SUCCESS', (int)$user['id'], null, 'auth', (int)$user['id'], 'Login OK', '', 'auth', '', '', 'info');
|
||||
|
||||
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.'];
|
||||
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.'];
|
||||
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.'];
|
||||
return ['success'=>false,'error'=>'Password must be at least 6 characters.'];
|
||||
if (empty($alias))
|
||||
return ['success' => false, 'error' => 'Alias is required.'];
|
||||
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.'];
|
||||
return ['success'=>false,'error'=>'A valid email address is required.'];
|
||||
|
||||
// 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.'];
|
||||
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.'];
|
||||
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.'];
|
||||
if ($s->fetch()) return ['success'=>false,'error'=>'Username already reserved. Try again in 24 hours.'];
|
||||
|
||||
// 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];
|
||||
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'];
|
||||
$rs = db()->prepare("SELECT id FROM users WHERE referral_code=? AND status='active'");
|
||||
$rs->execute([strtoupper(trim($referralCode))]);
|
||||
$ru = $rs->fetch();
|
||||
if ($ru) $referrerId = (int)$ru['id'];
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost'=>8]);
|
||||
$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]);
|
||||
$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,'mail_warning'=>true];
|
||||
}
|
||||
|
||||
return ['success' => true, 'email' => $email];
|
||||
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.'];
|
||||
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.'];
|
||||
}
|
||||
if (!$pending) return ['success'=>false,'error'=>'Verification link is invalid or has expired.'];
|
||||
|
||||
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']]);
|
||||
$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]);
|
||||
$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]);
|
||||
db()->prepare("INSERT IGNORE INTO referrals (referrer_id,referred_id,status) VALUES (?,?,'pending')")->execute([$pending['referred_by'],$userId]);
|
||||
db()->prepare("UPDATE users SET referred_by=? WHERE id=?")->execute([$pending['referred_by'],$userId]);
|
||||
} catch(Exception $e) {}
|
||||
}
|
||||
|
||||
// Delete pending row
|
||||
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
|
||||
|
||||
db()->commit();
|
||||
} catch (Exception $e) {
|
||||
} catch(Exception $e) {
|
||||
db()->rollBack();
|
||||
return ['success' => false, 'error' => 'Account creation failed. Please try again.'];
|
||||
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']];
|
||||
return ['success'=>true,'user_id'=>$userId,'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 {
|
||||
// ── Activity 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) {
|
||||
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);
|
||||
$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 */ }
|
||||
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,$adminId,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) {}
|
||||
}
|
||||
|
||||
// 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);
|
||||
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);
|
||||
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);
|
||||
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'; }
|
||||
return $v ?: '1.0.2';
|
||||
} catch(Exception $e) { return '1.0.2'; }
|
||||
}
|
||||
|
||||
// ── CSRF ───────────────────────────────────────────────────
|
||||
function getCsrfToken(): string {
|
||||
if (empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
function validateCsrfToken(string $token): bool {
|
||||
return !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user