Add user isolation mode and transfer features

Introduces user isolation mode, allowing domains, groups, and tags to be visible only to their owners when enabled. Adds user_id fields to domains and notification_groups, updates models and controllers for isolation-aware queries, and provides admin UI and endpoints for transferring domains and groups between users (single and bulk). Includes migration, settings UI, and routes for toggling isolation mode and handling data migration.
This commit is contained in:
Hosteroid
2025-10-20 17:04:13 +03:00
parent 52d20c2996
commit 6fbed15c7d
13 changed files with 825 additions and 61 deletions

View File

@@ -11,21 +11,28 @@ class Domain extends Model
/**
* Get all domains with their notification group
*/
public function getAllWithGroups(): array
public function getAllWithGroups(?int $userId = null): 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);
LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id";
if ($userId && !$this->isAdmin($userId)) {
$sql .= " WHERE d.user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId]);
} else {
$sql .= " 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
public function getExpiringDomains(int $days, ?int $userId = null): array
{
$sql = "SELECT d.*, ng.name as group_name
FROM domains d
@@ -33,27 +40,43 @@ class Domain extends Model
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";
AND d.expiration_date >= CURDATE()";
$params = [$days];
if ($userId && !$this->isAdmin($userId)) {
$sql .= " AND d.user_id = ?";
$params[] = $userId;
}
$sql .= " ORDER BY d.expiration_date ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([$days]);
$stmt->execute($params);
return $stmt->fetchAll();
}
/**
* Get domains by status
*/
public function getByStatus(string $status): array
public function getByStatus(string $status, ?int $userId = null): 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";
WHERE d.status = ?";
$params = [$status];
if ($userId && !$this->isAdmin($userId)) {
$sql .= " AND d.user_id = ?";
$params[] = $userId;
}
$sql .= " ORDER BY d.expiration_date ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([$status]);
$stmt->execute($params);
return $stmt->fetchAll();
}
@@ -100,24 +123,32 @@ class Domain extends Model
/**
* Get recent domains
*/
public function getRecent(int $limit = 5): array
public function getRecent(int $limit = 5, ?int $userId = null): 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 ?";
WHERE d.is_active = 1";
$params = [];
if ($userId && !$this->isAdmin($userId)) {
$sql .= " AND d.user_id = ?";
$params[] = $userId;
}
$sql .= " ORDER BY d.created_at DESC, d.id DESC LIMIT ?";
$params[] = $limit;
$stmt = $this->db->prepare($sql);
$stmt->execute([$limit]);
$stmt->execute($params);
return $stmt->fetchAll();
}
/**
* Get dashboard statistics
*/
public function getStatistics(): array
public function getStatistics(?int $userId = null): array
{
$stats = [
'total' => 0,
@@ -127,9 +158,19 @@ class Domain extends Model
'inactive' => 0,
];
// Build WHERE clause for user filtering
$whereClause = "WHERE is_active = 1";
$params = [];
if ($userId && !$this->isAdmin($userId)) {
$whereClause .= " AND user_id = ?";
$params[] = $userId;
}
// Get status counts for active domains only
$sql = "SELECT status, COUNT(*) as count FROM domains WHERE is_active = 1 GROUP BY status";
$stmt = $this->db->query($sql);
$sql = "SELECT status, COUNT(*) as count FROM domains $whereClause GROUP BY status";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$results = $stmt->fetchAll();
$stats['total'] = array_sum(array_column($results, 'count'));
@@ -139,7 +180,16 @@ class Domain extends Model
}
// Get count of inactive domains (is_active = 0)
$inactiveStmt = $this->db->query("SELECT COUNT(*) as count FROM domains WHERE is_active = 0");
$inactiveWhereClause = "WHERE is_active = 0";
$inactiveParams = [];
if ($userId && !$this->isAdmin($userId)) {
$inactiveWhereClause .= " AND user_id = ?";
$inactiveParams[] = $userId;
}
$inactiveStmt = $this->db->prepare("SELECT COUNT(*) as count FROM domains $inactiveWhereClause");
$inactiveStmt->execute($inactiveParams);
$inactiveResult = $inactiveStmt->fetch();
$stats['inactive'] = $inactiveResult['count'] ?? 0;
@@ -152,10 +202,10 @@ class Domain extends Model
/**
* Get filtered, sorted, and paginated domains
*/
public function getFilteredPaginated(array $filters, string $sortBy, string $sortOrder, int $page, int $perPage, int $expiringThreshold = 30): array
public function getFilteredPaginated(array $filters, string $sortBy, string $sortOrder, int $page, int $perPage, int $expiringThreshold = 30, ?int $userId = null): array
{
// Get all domains with groups
$domains = $this->getAllWithGroups();
$domains = $this->getAllWithGroups($userId);
// Apply search filter
if (!empty($filters['search'])) {
@@ -242,9 +292,18 @@ class Domain extends Model
/**
* Get all unique tags from all domains
*/
public function getAllTags(): array
public function getAllTags(?int $userId = null): array
{
$stmt = $this->db->query("SELECT DISTINCT tags FROM domains WHERE tags IS NOT NULL AND tags != ''");
$sql = "SELECT DISTINCT tags FROM domains WHERE tags IS NOT NULL AND tags != ''";
$params = [];
if ($userId && !$this->isAdmin($userId)) {
$sql .= " AND user_id = ?";
$params[] = $userId;
}
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$results = $stmt->fetchAll();
$allTags = [];
@@ -260,5 +319,29 @@ class Domain extends Model
sort($allTags);
return $allTags;
}
/**
* Check if user is admin
*/
private function isAdmin(?int $userId): bool
{
if (!$userId) {
return false;
}
$stmt = $this->db->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
return $user && $user['role'] === 'admin';
}
/**
* Get first admin user
*/
public function getFirstAdminUser(): ?array
{
$stmt = $this->db->query("SELECT * FROM users WHERE role = 'admin' ORDER BY id ASC LIMIT 1");
return $stmt->fetch() ?: null;
}
}

View File

@@ -11,25 +11,31 @@ class NotificationGroup extends Model
/**
* Get all groups with channel count
*/
public function getAllWithChannelCount(): array
public function getAllWithChannelCount(?int $userId = null): 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);
LEFT JOIN domains d ON ng.id = d.notification_group_id";
if ($userId && !$this->isAdmin($userId)) {
$sql .= " WHERE ng.user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId]);
} else {
$sql .= " 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
public function getWithDetails(int $id, ?int $userId = null): ?array
{
$group = $this->find($id);
@@ -37,13 +43,22 @@ class NotificationGroup extends Model
return null;
}
// Check if user has access to this group
if ($userId && !$this->isAdmin($userId) && $group['user_id'] != $userId) {
return null;
}
// Get channels
$channelModel = new NotificationChannel();
$group['channels'] = $channelModel->getByGroupId($id);
// Get domains
// Get domains (filtered by user if needed)
$domainModel = new Domain();
$group['domains'] = $domainModel->where('notification_group_id', $id);
if ($userId && !$this->isAdmin($userId)) {
$group['domains'] = $domainModel->where('notification_group_id', $id, $userId);
} else {
$group['domains'] = $domainModel->where('notification_group_id', $id);
}
return $group;
}
@@ -61,5 +76,29 @@ class NotificationGroup extends Model
return $this->delete($id);
}
/**
* Check if user is admin
*/
private function isAdmin(?int $userId): bool
{
if (!$userId) {
return false;
}
$stmt = $this->db->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
return $user && $user['role'] === 'admin';
}
/**
* Get first admin user
*/
public function getFirstAdminUser(): ?array
{
$stmt = $this->db->query("SELECT * FROM users WHERE role = 'admin' ORDER BY id ASC LIMIT 1");
return $stmt->fetch() ?: null;
}
}