Initial Commit

This commit is contained in:
Hosteroid
2025-10-08 14:23:07 +03:00
commit b3b3ac66ff
78 changed files with 14248 additions and 0 deletions

143
app/Models/Domain.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
namespace App\Models;
use Core\Model;
class Domain extends Model
{
protected static string $table = 'domains';
/**
* Get all domains with their notification group
*/
public function getAllWithGroups(): array
{
$sql = "SELECT d.*, ng.name as group_name
FROM domains d
LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id
ORDER BY d.status DESC, d.expiration_date ASC";
$stmt = $this->db->query($sql);
return $stmt->fetchAll();
}
/**
* Get domains expiring within days
*/
public function getExpiringDomains(int $days): array
{
$sql = "SELECT d.*, ng.name as group_name
FROM domains d
LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id
WHERE d.is_active = 1
AND d.expiration_date IS NOT NULL
AND d.expiration_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND d.expiration_date >= CURDATE()
ORDER BY d.expiration_date ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([$days]);
return $stmt->fetchAll();
}
/**
* Get domains by status
*/
public function getByStatus(string $status): array
{
$sql = "SELECT d.*, ng.name as group_name
FROM domains d
LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id
WHERE d.status = ?
ORDER BY d.expiration_date ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([$status]);
return $stmt->fetchAll();
}
/**
* Get domain with notification channels
*/
public function getWithChannels(int $id): ?array
{
$sql = "SELECT d.*, ng.name as group_name, ng.id as group_id
FROM domains d
LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id
WHERE d.id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$id]);
$domain = $stmt->fetch();
if (!$domain) {
return null;
}
// Get notification channels for this domain's group
if ($domain['group_id']) {
$channelModel = new NotificationChannel();
$domain['channels'] = $channelModel->getByGroupId($domain['group_id']);
} else {
$domain['channels'] = [];
}
return $domain;
}
/**
* Check if domain exists
*/
public function existsByDomain(string $domainName): bool
{
$stmt = $this->db->prepare("SELECT COUNT(*) as count FROM domains WHERE domain_name = ?");
$stmt->execute([$domainName]);
$result = $stmt->fetch();
return $result['count'] > 0;
}
/**
* Get recent domains
*/
public function getRecent(int $limit = 5): array
{
$sql = "SELECT d.*, ng.name as group_name
FROM domains d
LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id
WHERE d.is_active = 1
ORDER BY d.created_at DESC, d.id DESC
LIMIT ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
/**
* Get dashboard statistics
*/
public function getStatistics(): array
{
$stats = [
'total' => 0,
'active' => 0,
'expiring_soon' => 0,
'expired' => 0,
'inactive' => 0,
];
$sql = "SELECT status, COUNT(*) as count FROM domains WHERE is_active = 1 GROUP BY status";
$stmt = $this->db->query($sql);
$results = $stmt->fetchAll();
$stats['total'] = array_sum(array_column($results, 'count'));
foreach ($results as $row) {
$stats[strtolower($row['status'])] = $row['count'];
}
return $stats;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Core\Model;
class NotificationChannel extends Model
{
protected static string $table = 'notification_channels';
/**
* Get channels by notification group ID
*/
public function getByGroupId(int $groupId): array
{
return $this->where('notification_group_id', $groupId);
}
/**
* Get active channels by notification group ID
*/
public function getActiveByGroupId(int $groupId): array
{
$sql = "SELECT * FROM notification_channels
WHERE notification_group_id = ? AND is_active = 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([$groupId]);
return $stmt->fetchAll();
}
/**
* Create channel with JSON config
*/
public function createChannel(int $groupId, string $type, array $config): int
{
return $this->create([
'notification_group_id' => $groupId,
'channel_type' => $type,
'channel_config' => json_encode($config),
'is_active' => 1
]);
}
/**
* Update channel config
*/
public function updateConfig(int $id, array $config): bool
{
$sql = "UPDATE notification_channels SET channel_config = ?, updated_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([json_encode($config), $id]);
}
/**
* Toggle channel active status
*/
public function toggleActive(int $id): bool
{
$sql = "UPDATE notification_channels
SET is_active = NOT is_active, updated_at = NOW()
WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$id]);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Models;
use Core\Model;
class NotificationGroup extends Model
{
protected static string $table = 'notification_groups';
/**
* Get all groups with channel count
*/
public function getAllWithChannelCount(): array
{
$sql = "SELECT ng.*,
COUNT(DISTINCT nc.id) as channel_count,
COUNT(DISTINCT d.id) as domain_count
FROM notification_groups ng
LEFT JOIN notification_channels nc ON ng.id = nc.notification_group_id
LEFT JOIN domains d ON ng.id = d.notification_group_id
GROUP BY ng.id
ORDER BY ng.name ASC";
$stmt = $this->db->query($sql);
return $stmt->fetchAll();
}
/**
* Get group with channels and domains
*/
public function getWithDetails(int $id): ?array
{
$group = $this->find($id);
if (!$group) {
return null;
}
// Get channels
$channelModel = new NotificationChannel();
$group['channels'] = $channelModel->getByGroupId($id);
// Get domains
$domainModel = new Domain();
$group['domains'] = $domainModel->where('notification_group_id', $id);
return $group;
}
/**
* Delete group and handle relationships
*/
public function deleteWithRelations(int $id): bool
{
// The database CASCADE will handle channels
// But we need to set domains to NULL
$sql = "UPDATE domains SET notification_group_id = NULL WHERE notification_group_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$id]);
return $this->delete($id);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models;
use Core\Model;
class NotificationLog extends Model
{
protected static string $table = 'notification_logs';
/**
* Log a notification
*/
public function log(int $domainId, string $type, string $channel, string $message, bool $success, ?string $error = null): int
{
return $this->create([
'domain_id' => $domainId,
'notification_type' => $type,
'channel_type' => $channel,
'message' => $message,
'status' => $success ? 'sent' : 'failed',
'error_message' => $error
]);
}
/**
* Get logs for a domain
*/
public function getByDomain(int $domainId, int $limit = 50): array
{
$sql = "SELECT * FROM notification_logs
WHERE domain_id = ?
ORDER BY sent_at DESC
LIMIT ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$domainId, $limit]);
return $stmt->fetchAll();
}
/**
* Get recent logs
*/
public function getRecent(int $limit = 100): array
{
$sql = "SELECT nl.*, d.domain_name
FROM notification_logs nl
JOIN domains d ON nl.domain_id = d.id
ORDER BY nl.sent_at DESC
LIMIT ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
/**
* Check if notification was sent recently
*/
public function wasSentRecently(int $domainId, string $type, int $hoursAgo = 24): bool
{
$sql = "SELECT COUNT(*) as count FROM notification_logs
WHERE domain_id = ?
AND notification_type = ?
AND status = 'sent'
AND sent_at >= DATE_SUB(NOW(), INTERVAL ? HOUR)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$domainId, $type, $hoursAgo]);
$result = $stmt->fetch();
return $result['count'] > 0;
}
}

155
app/Models/TldImportLog.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
namespace App\Models;
use Core\Model;
class TldImportLog extends Model
{
protected static string $table = 'tld_import_logs';
/**
* Create a new import log entry
*/
public function startImport(string $importType, ?string $ianaPublicationDate = null): int
{
return $this->create([
'import_type' => $importType,
'iana_publication_date' => $ianaPublicationDate,
'status' => 'running',
'started_at' => date('Y-m-d H:i:s')
]);
}
/**
* Complete an import log entry
*/
public function completeImport(int $logId, array $stats, ?string $status = null, ?string $errorMessage = null, ?array $details = null): bool
{
$data = [
'total_tlds' => $stats['total_tlds'] ?? 0,
'new_tlds' => $stats['new_tlds'] ?? 0,
'updated_tlds' => $stats['updated_tlds'] ?? 0,
'failed_tlds' => $stats['failed_tlds'] ?? 0,
'completed_at' => date('Y-m-d H:i:s'),
'status' => $status ?? ($errorMessage ? 'failed' : 'completed'),
'error_message' => $errorMessage
];
if ($details !== null) {
$data['details'] = json_encode($details);
}
return $this->update($logId, $data);
}
/**
* Update an import log entry (for progress tracking)
*/
public function update(int $logId, array $data, ?string $status = null, ?string $errorMessage = null, ?array $details = null): bool
{
if ($status !== null) {
$data['status'] = $status;
}
if ($errorMessage !== null) {
$data['error_message'] = $errorMessage;
}
if ($details !== null) {
$data['details'] = json_encode($details);
}
return parent::update($logId, $data);
}
/**
* Get recent import logs
*/
public function getRecent(int $limit = 10): array
{
$sql = "SELECT *,
COALESCE(new_tlds, 0) as new_tlds
FROM tld_import_logs
ORDER BY started_at DESC
LIMIT ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
/**
* Get import statistics
*/
public function getImportStatistics(): array
{
$stats = [
'total_imports' => 0,
'successful_imports' => 0,
'failed_imports' => 0,
'last_import' => null,
'total_tlds_imported' => 0
];
// Total imports
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_import_logs");
$stats['total_imports'] = $stmt->fetch()['count'];
// Successful imports
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_import_logs WHERE status = 'completed'");
$stats['successful_imports'] = $stmt->fetch()['count'];
// Failed imports
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_import_logs WHERE status = 'failed'");
$stats['failed_imports'] = $stmt->fetch()['count'];
// Last import
$stmt = $this->db->query("SELECT * FROM tld_import_logs ORDER BY started_at DESC LIMIT 1");
$lastImport = $stmt->fetch();
if ($lastImport) {
$stats['last_import'] = $lastImport['started_at'];
}
// Total TLDs imported
$stmt = $this->db->query("SELECT SUM(total_tlds) as total FROM tld_import_logs WHERE status = 'completed'");
$result = $stmt->fetch();
$stats['total_tlds_imported'] = $result['total'] ?? 0;
return $stats;
}
/**
* Get import logs with pagination
*/
public function getPaginated(int $page = 1, int $perPage = 20): array
{
$offset = ($page - 1) * $perPage;
$sql = "SELECT *,
COALESCE(new_tlds, 0) as new_tlds
FROM tld_import_logs
ORDER BY started_at DESC
LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$perPage, $offset]);
$logs = $stmt->fetchAll();
// Get total count
$countStmt = $this->db->query("SELECT COUNT(*) as count FROM tld_import_logs");
$total = $countStmt->fetch()['count'];
return [
'logs' => $logs,
'pagination' => [
'current_page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => ceil($total / $perPage),
'showing_from' => $total > 0 ? $offset + 1 : 0,
'showing_to' => min($offset + $perPage, $total)
]
];
}
}

253
app/Models/TldRegistry.php Normal file
View File

@@ -0,0 +1,253 @@
<?php
namespace App\Models;
use Core\Model;
class TldRegistry extends Model
{
protected static string $table = 'tld_registry';
/**
* Get TLD by domain extension
*/
public function getByTld(string $tld): ?array
{
// Ensure TLD starts with dot
if (!str_starts_with($tld, '.')) {
$tld = '.' . $tld;
}
$stmt = $this->db->prepare("SELECT * FROM tld_registry WHERE tld = ? AND is_active = 1");
$stmt->execute([$tld]);
return $stmt->fetch() ?: null;
}
/**
* Get all active TLDs
*/
public function getAllActive(): array
{
$stmt = $this->db->query("SELECT * FROM tld_registry WHERE is_active = 1 ORDER BY tld ASC");
return $stmt->fetchAll();
}
/**
* Get TLDs that need updating (older than specified days)
*/
public function getTldsNeedingUpdate(int $daysOld = 30): array
{
$sql = "SELECT * FROM tld_registry
WHERE is_active = 1
AND (updated_at < DATE_SUB(NOW(), INTERVAL ? DAY)
OR updated_at IS NULL)
ORDER BY updated_at ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([$daysOld]);
return $stmt->fetchAll();
}
/**
* Create or update TLD registry entry
*/
public function createOrUpdate(array $data): int
{
$tld = $data['tld'];
// Check if TLD already exists
$existing = $this->getByTld($tld);
if ($existing) {
// Update existing record
$this->update($existing['id'], $data);
return $existing['id'];
} else {
// Create new record
return $this->create($data);
}
}
/**
* Get TLD statistics
*/
public function getStatistics(): array
{
$stats = [
'total' => 0,
'active' => 0,
'with_rdap' => 0,
'with_whois' => 0,
'recently_updated' => 0,
'needs_update' => 0
];
// Total TLDs
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_registry");
$stats['total'] = $stmt->fetch()['count'];
// Active TLDs
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_registry WHERE is_active = 1");
$stats['active'] = $stmt->fetch()['count'];
// TLDs with RDAP servers
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_registry WHERE rdap_servers IS NOT NULL AND rdap_servers != '[]' AND is_active = 1");
$stats['with_rdap'] = $stmt->fetch()['count'];
// TLDs with WHOIS servers
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_registry WHERE whois_server IS NOT NULL AND whois_server != '' AND is_active = 1");
$stats['with_whois'] = $stmt->fetch()['count'];
// Recently updated (last 7 days)
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_registry WHERE updated_at > DATE_SUB(NOW(), INTERVAL 7 DAY) AND is_active = 1");
$stats['recently_updated'] = $stmt->fetch()['count'];
// Needs update (older than 30 days)
$stmt = $this->db->query("SELECT COUNT(*) as count FROM tld_registry WHERE updated_at < DATE_SUB(NOW(), INTERVAL 30 DAY) AND is_active = 1");
$stats['needs_update'] = $stmt->fetch()['count'];
return $stats;
}
/**
* Get TLDs by search term
*/
public function search(string $search): array
{
$search = '%' . $search . '%';
$sql = "SELECT * FROM tld_registry
WHERE (LOWER(tld) LIKE LOWER(?) OR LOWER(whois_server) LIKE LOWER(?) OR LOWER(registry_url) LIKE LOWER(?))
ORDER BY tld ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([$search, $search, $search]);
return $stmt->fetchAll();
}
/**
* Get TLDs with pagination and sorting
*/
public function getPaginated(int $page = 1, int $perPage = 50, string $search = '', string $sort = 'tld', string $order = 'asc', string $status = '', string $dataType = ''): array
{
$offset = ($page - 1) * $perPage;
// Validate sort column
$allowedSorts = ['tld', 'rdap_servers', 'whois_server', 'updated_at', 'is_active'];
if (!in_array($sort, $allowedSorts)) {
$sort = 'tld';
}
// Validate order
$order = strtolower($order) === 'desc' ? 'DESC' : 'ASC';
// Build WHERE clause
$whereConditions = [];
$params = [];
// Search filter
if (!empty($search)) {
$searchParam = '%' . $search . '%';
$whereConditions[] = "(LOWER(tld) LIKE LOWER(?) OR LOWER(whois_server) LIKE LOWER(?) OR LOWER(registry_url) LIKE LOWER(?))";
$params = array_merge($params, [$searchParam, $searchParam, $searchParam]);
}
// Status filter
if ($status === 'active') {
$whereConditions[] = "is_active = 1";
} elseif ($status === 'inactive') {
$whereConditions[] = "is_active = 0";
}
// Data type filter
if ($dataType === 'with_rdap') {
$whereConditions[] = "(rdap_servers IS NOT NULL AND rdap_servers != '' AND rdap_servers != '[]')";
} elseif ($dataType === 'with_whois') {
$whereConditions[] = "(whois_server IS NOT NULL AND whois_server != '')";
} elseif ($dataType === 'with_registry') {
$whereConditions[] = "(registry_url IS NOT NULL AND registry_url != '')";
} elseif ($dataType === 'missing_data') {
$whereConditions[] = "((rdap_servers IS NULL OR rdap_servers = '' OR rdap_servers = '[]') AND (whois_server IS NULL OR whois_server = '') AND (registry_url IS NULL OR registry_url = ''))";
}
$whereClause = !empty($whereConditions) ? 'WHERE ' . implode(' AND ', $whereConditions) : '';
// Build ORDER BY clause
$orderBy = "ORDER BY $sort $order";
if ($sort === 'tld') {
$orderBy .= ", tld ASC"; // Secondary sort for consistent results
}
// Build main query
$sql = "SELECT * FROM tld_registry $whereClause $orderBy LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql);
$stmt->execute(array_merge($params, [$perPage, $offset]));
$tlds = $stmt->fetchAll();
// Get total count
$countSql = "SELECT COUNT(*) as count FROM tld_registry $whereClause";
$countStmt = $this->db->prepare($countSql);
$countStmt->execute($params);
$total = $countStmt->fetch()['count'];
return [
'tlds' => $tlds,
'pagination' => [
'current_page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => ceil($total / $perPage),
'showing_from' => $total > 0 ? $offset + 1 : 0,
'showing_to' => min($offset + $perPage, $total)
]
];
}
/**
* Toggle TLD active status
*/
public function toggleActive(int $id): bool
{
$sql = "UPDATE tld_registry SET is_active = NOT is_active, updated_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$id]);
}
/**
* Get TLDs that have RDAP servers
*/
public function getTldsWithRdap(): array
{
$sql = "SELECT * FROM tld_registry
WHERE rdap_servers IS NOT NULL
AND rdap_servers != '[]'
AND is_active = 1
ORDER BY tld ASC";
$stmt = $this->db->query($sql);
return $stmt->fetchAll();
}
/**
* Get TLDs that have WHOIS servers
*/
public function getTldsWithWhois(): array
{
$sql = "SELECT * FROM tld_registry
WHERE whois_server IS NOT NULL
AND whois_server != ''
AND is_active = 1
ORDER BY tld ASC";
$stmt = $this->db->query($sql);
return $stmt->fetchAll();
}
/**
* Execute a custom SQL query
*/
public function query(string $sql): array
{
$stmt = $this->db->query($sql);
return $stmt->fetchAll();
}
}

65
app/Models/User.php Normal file
View File

@@ -0,0 +1,65 @@
<?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]);
}
}