Files
2026-05-15 16:46:08 -05:00

210 lines
9.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/mailer.php';
function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
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: /admin/login.php'); 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'])) {
logActivity('LOGIN_FAILED', null, null, 'auth', 0, 'Failed login: '.$username, '', 'security', '', '', 'warning');
return ['success' => false, 'error' => 'Invalid username or password.'];
}
if (!$user['email_verified'] && !$user['is_admin']) {
return ['success'=>false,'error'=>'Please verify your email address before logging in.','unverified'=>true,'email'=>$user['email']];
}
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];
}
function initiateRegistration(string $username, string $password, string $alias, string $email, string $referralCode = ''): array {
$username = strtolower(trim($username));
$alias = trim($alias);
$email = strtolower(trim($email));
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.'];
$s = db()->prepare("SELECT id FROM users WHERE username=?");
$s->execute([$username]);
if ($s->fetch()) return ['success'=>false,'error'=>'Username already taken.'];
$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.'];
$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.'];
$s = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW()");
$s->execute([$email]);
$existing = $s->fetch();
if ($existing) {
sendVerificationEmail($email, $alias, $existing['token']);
return ['success'=>true,'resent'=>true,'email'=>$email];
}
db()->prepare("DELETE FROM pending_registrations WHERE email=? OR (username=? AND expires_at <= NOW())")->execute([$email, $username]);
$referrerId = null;
if ($referralCode) {
$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, ['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]);
$sent = sendVerificationEmail($email, $alias, $token);
if (!$sent) {
return ['success'=>true,'email'=>$email,'mail_warning'=>true];
}
return ['success'=>true,'email'=>$email];
}
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'=>'Verification link is invalid or has expired.'];
db()->beginTransaction();
try {
$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();
$code = strtoupper(substr(md5($userId.uniqid()),0,8));
db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code,$userId]);
if (!empty($pending['referred_by'])) {
try {
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) {}
}
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.'];
}
return ['success'=>true,'user_id'=>$userId,'username'=>$pending['username'],'alias'=>$pending['alias']];
}
// ── 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 {
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,$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) {}
}
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);
}
function getAppVersion(): string {
try {
$v = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn();
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);
}