2025-10-08 14:23:07 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
|
|
|
|
use Core\Model;
|
|
|
|
|
|
|
|
|
|
class User extends Model
|
|
|
|
|
{
|
|
|
|
|
protected static string $table = 'users';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find user by username
|
|
|
|
|
*/
|
|
|
|
|
public function findByUsername(string $username): ?array
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare("SELECT * FROM users WHERE username = ? AND is_active = 1");
|
|
|
|
|
$stmt->execute([$username]);
|
|
|
|
|
$result = $stmt->fetch();
|
|
|
|
|
return $result ?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Verify password
|
|
|
|
|
*/
|
|
|
|
|
public function verifyPassword(string $password, string $hash): bool
|
|
|
|
|
{
|
|
|
|
|
return password_verify($password, $hash);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update last login timestamp
|
|
|
|
|
*/
|
|
|
|
|
public function updateLastLogin(int $userId): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
|
|
|
|
|
return $stmt->execute([$userId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create user with hashed password
|
|
|
|
|
*/
|
|
|
|
|
public function createUser(string $username, string $password, ?string $email = null, ?string $fullName = null): int
|
|
|
|
|
{
|
|
|
|
|
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
|
|
|
|
|
|
|
|
|
return $this->create([
|
|
|
|
|
'username' => $username,
|
|
|
|
|
'password' => $hashedPassword,
|
|
|
|
|
'email' => $email,
|
|
|
|
|
'full_name' => $fullName,
|
|
|
|
|
'is_active' => 1
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Change password
|
|
|
|
|
*/
|
|
|
|
|
public function changePassword(int $userId, string $newPassword): bool
|
|
|
|
|
{
|
|
|
|
|
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
|
|
|
|
$stmt = $this->db->prepare("UPDATE users SET password = ? WHERE id = ?");
|
|
|
|
|
return $stmt->execute([$hashedPassword, $userId]);
|
|
|
|
|
}
|
Upgraded to 1.1.0
1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
2025-10-09 18:02:46 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get users with filters, sorting, and pagination
|
|
|
|
|
*/
|
|
|
|
|
public function getFiltered(array $filters = [], string $sort = 'username', string $order = 'ASC', int $limit = 25, int $offset = 0): array
|
|
|
|
|
{
|
|
|
|
|
$query = "SELECT * FROM users WHERE 1=1";
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
// Apply search filter
|
|
|
|
|
if (!empty($filters['search'])) {
|
|
|
|
|
$query .= " AND (username LIKE ? OR email LIKE ? OR full_name LIKE ?)";
|
|
|
|
|
$searchTerm = "%{$filters['search']}%";
|
|
|
|
|
$params[] = $searchTerm;
|
|
|
|
|
$params[] = $searchTerm;
|
|
|
|
|
$params[] = $searchTerm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply role filter
|
|
|
|
|
if (!empty($filters['role'])) {
|
|
|
|
|
$query .= " AND role = ?";
|
|
|
|
|
$params[] = $filters['role'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply status filter
|
|
|
|
|
if (isset($filters['status']) && $filters['status'] !== '') {
|
|
|
|
|
$isActive = ($filters['status'] === 'active') ? 1 : 0;
|
|
|
|
|
$query .= " AND is_active = ?";
|
|
|
|
|
$params[] = $isActive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply sorting
|
|
|
|
|
$allowedSortColumns = ['username', 'email', 'full_name', 'role', 'is_active', 'email_verified', 'last_login', 'created_at'];
|
|
|
|
|
if (!in_array($sort, $allowedSortColumns)) {
|
|
|
|
|
$sort = 'username';
|
|
|
|
|
}
|
|
|
|
|
$order = strtoupper($order) === 'DESC' ? 'DESC' : 'ASC';
|
|
|
|
|
$query .= " ORDER BY {$sort} {$order}";
|
|
|
|
|
|
|
|
|
|
// Apply pagination
|
|
|
|
|
$query .= " LIMIT ? OFFSET ?";
|
|
|
|
|
$params[] = $limit;
|
|
|
|
|
$params[] = $offset;
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($query);
|
|
|
|
|
$stmt->execute($params);
|
|
|
|
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Count users with filters
|
|
|
|
|
*/
|
|
|
|
|
public function countFiltered(array $filters = []): int
|
|
|
|
|
{
|
|
|
|
|
$query = "SELECT COUNT(*) as total FROM users WHERE 1=1";
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
// Apply search filter
|
|
|
|
|
if (!empty($filters['search'])) {
|
|
|
|
|
$query .= " AND (username LIKE ? OR email LIKE ? OR full_name LIKE ?)";
|
|
|
|
|
$searchTerm = "%{$filters['search']}%";
|
|
|
|
|
$params[] = $searchTerm;
|
|
|
|
|
$params[] = $searchTerm;
|
|
|
|
|
$params[] = $searchTerm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply role filter
|
|
|
|
|
if (!empty($filters['role'])) {
|
|
|
|
|
$query .= " AND role = ?";
|
|
|
|
|
$params[] = $filters['role'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply status filter
|
|
|
|
|
if (isset($filters['status']) && $filters['status'] !== '') {
|
|
|
|
|
$isActive = ($filters['status'] === 'active') ? 1 : 0;
|
|
|
|
|
$query .= " AND is_active = ?";
|
|
|
|
|
$params[] = $isActive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($query);
|
|
|
|
|
$stmt->execute($params);
|
|
|
|
|
return (int)$stmt->fetch(\PDO::FETCH_ASSOC)['total'];
|
|
|
|
|
}
|
2025-10-10 14:01:19 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update email verification token
|
|
|
|
|
*/
|
|
|
|
|
public function updateEmailVerificationToken(int $userId, string $token): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare(
|
|
|
|
|
"UPDATE users SET email_verification_token = ?, email_verification_sent_at = NOW() WHERE id = ?"
|
|
|
|
|
);
|
|
|
|
|
return $stmt->execute([$token, $userId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark email as verified
|
|
|
|
|
*/
|
|
|
|
|
public function markEmailAsVerified(int $userId): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare("UPDATE users SET email_verified = 1 WHERE id = ?");
|
|
|
|
|
return $stmt->execute([$userId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Verify email by clearing token
|
|
|
|
|
*/
|
|
|
|
|
public function verifyEmailByToken(int $userId): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare(
|
|
|
|
|
"UPDATE users SET email_verified = 1, email_verification_token = NULL WHERE id = ?"
|
|
|
|
|
);
|
|
|
|
|
return $stmt->execute([$userId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find user by email verification token
|
|
|
|
|
*/
|
|
|
|
|
public function findByVerificationToken(string $token): ?array
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare(
|
Add two-factor authentication (2FA) support
Introduces two-factor authentication (2FA) with TOTP, backup codes, and email codes. Adds controllers, services, views, and migration for 2FA setup, verification, and management. Updates user and settings models, email helper, and relevant controllers to support 2FA policy enforcement, configuration, and user flows. Enhances security by allowing admins to require or disable 2FA, and provides backup code generation and management for account recovery.
2025-10-16 17:25:06 +03:00
|
|
|
"SELECT * FROM users WHERE email_verification_token = ? AND (email_verified IS NULL OR email_verified = 0)"
|
2025-10-10 14:01:19 +03:00
|
|
|
);
|
|
|
|
|
$stmt->execute([$token]);
|
|
|
|
|
$result = $stmt->fetch();
|
|
|
|
|
return $result ?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create password reset token
|
|
|
|
|
*/
|
|
|
|
|
public function createPasswordResetToken(int $userId, string $token, string $expiresAt): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare(
|
|
|
|
|
"INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)"
|
|
|
|
|
);
|
|
|
|
|
return $stmt->execute([$userId, $token, $expiresAt]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find valid password reset token
|
|
|
|
|
*/
|
|
|
|
|
public function findPasswordResetToken(string $token): ?array
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare(
|
|
|
|
|
"SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND expires_at > NOW()"
|
|
|
|
|
);
|
|
|
|
|
$stmt->execute([$token]);
|
|
|
|
|
$result = $stmt->fetch();
|
|
|
|
|
return $result ?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark password reset token as used
|
|
|
|
|
*/
|
|
|
|
|
public function markPasswordResetTokenAsUsed(int $tokenId): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare("UPDATE password_reset_tokens SET used = 1 WHERE id = ?");
|
|
|
|
|
return $stmt->execute([$tokenId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create remember token
|
|
|
|
|
*/
|
|
|
|
|
public function createRememberToken(int $userId, string $sessionId, string $token, string $expiresAt): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare(
|
|
|
|
|
"INSERT INTO remember_tokens (user_id, session_id, token, expires_at) VALUES (?, ?, ?, ?)"
|
|
|
|
|
);
|
|
|
|
|
return $stmt->execute([$userId, $sessionId, $token, $expiresAt]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find user by remember token
|
|
|
|
|
*/
|
|
|
|
|
public function findByRememberToken(string $token): ?array
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare(
|
|
|
|
|
"SELECT user_id FROM remember_tokens WHERE token = ? AND expires_at > NOW()"
|
|
|
|
|
);
|
|
|
|
|
$stmt->execute([$token]);
|
|
|
|
|
$result = $stmt->fetch();
|
|
|
|
|
return $result ?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete remember token
|
|
|
|
|
*/
|
|
|
|
|
public function deleteRememberToken(string $token): bool
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare("DELETE FROM remember_tokens WHERE token = ?");
|
|
|
|
|
return $stmt->execute([$token]);
|
|
|
|
|
}
|
Add two-factor authentication (2FA) support
Introduces two-factor authentication (2FA) with TOTP, backup codes, and email codes. Adds controllers, services, views, and migration for 2FA setup, verification, and management. Updates user and settings models, email helper, and relevant controllers to support 2FA policy enforcement, configuration, and user flows. Enhances security by allowing admins to require or disable 2FA, and provides backup code generation and management for account recovery.
2025-10-16 17:25:06 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get user's 2FA status
|
|
|
|
|
*/
|
|
|
|
|
public function getTwoFactorStatus(int $userId): array
|
|
|
|
|
{
|
|
|
|
|
$user = $this->find($userId);
|
|
|
|
|
if (!$user) {
|
|
|
|
|
return ['enabled' => false, 'can_enable' => false, 'required' => false];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$twoFactorService = new \App\Services\TwoFactorService();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'enabled' => (bool)$user['two_factor_enabled'],
|
|
|
|
|
'can_enable' => $twoFactorService->canEnableTwoFactor($userId),
|
|
|
|
|
'required' => $twoFactorService->isTwoFactorRequired($userId),
|
|
|
|
|
'setup_at' => $user['two_factor_setup_at'],
|
|
|
|
|
'backup_codes_count' => $user['two_factor_backup_codes'] ? count(json_decode($user['two_factor_backup_codes'], true)) : 0
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if user has verified email (required for 2FA)
|
|
|
|
|
*/
|
|
|
|
|
public function hasVerifiedEmail(int $userId): bool
|
|
|
|
|
{
|
|
|
|
|
$user = $this->find($userId);
|
|
|
|
|
return $user && $user['email_verified'];
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
}
|
|
|
|
|
|