2025-11-18 00:03:24 +00:00
|
|
|
import { Component, OnInit, inject, signal } from '@angular/core';
|
|
|
|
|
import { CommonModule } from '@angular/common';
|
2025-11-24 01:31:15 +00:00
|
|
|
import { RouterLink } from '@angular/router';
|
2025-11-18 00:03:24 +00:00
|
|
|
import { ApiService } from '../../core/services/api.service';
|
2025-11-24 01:31:15 +00:00
|
|
|
import { ToastService } from '../../core/services/toast.service';
|
|
|
|
|
|
|
|
|
|
interface DomainView {
|
|
|
|
|
domain: {
|
|
|
|
|
id: number;
|
|
|
|
|
domain: string;
|
|
|
|
|
dnsProvider: 'cloudflare' | 'manual' | null;
|
|
|
|
|
isObsolete: boolean;
|
|
|
|
|
defaultWildcard: boolean;
|
|
|
|
|
};
|
|
|
|
|
serviceCount: number;
|
|
|
|
|
certificateStatus: 'valid' | 'expiring-soon' | 'expired' | 'pending' | 'none';
|
|
|
|
|
daysRemaining: number | null;
|
|
|
|
|
certificates: any[];
|
|
|
|
|
requirements: any[];
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
|
|
|
|
|
@Component({
|
2025-11-24 01:31:15 +00:00
|
|
|
selector: 'app-domains',
|
2025-11-18 00:03:24 +00:00
|
|
|
standalone: true,
|
2025-11-24 01:31:15 +00:00
|
|
|
imports: [CommonModule, RouterLink],
|
2025-11-18 00:03:24 +00:00
|
|
|
template: `
|
|
|
|
|
<div class="px-4 sm:px-0">
|
2025-11-24 01:31:15 +00:00
|
|
|
<div class="flex justify-between items-center mb-8">
|
|
|
|
|
<h1 class="text-3xl font-bold text-gray-900">Domains</h1>
|
|
|
|
|
<button
|
|
|
|
|
(click)="syncDomains()"
|
|
|
|
|
[disabled]="syncing()"
|
|
|
|
|
class="btn btn-primary"
|
|
|
|
|
>
|
|
|
|
|
{{ syncing() ? 'Syncing...' : 'Sync Cloudflare' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-11-18 00:03:24 +00:00
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
@if (loading()) {
|
|
|
|
|
<div class="card text-center py-12">
|
|
|
|
|
<p class="text-gray-500">Loading domains...</p>
|
|
|
|
|
</div>
|
|
|
|
|
} @else if (domains().length > 0) {
|
2025-11-18 00:03:24 +00:00
|
|
|
<div class="card overflow-hidden p-0">
|
|
|
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
|
|
|
<thead class="bg-gray-50">
|
|
|
|
|
<tr>
|
|
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Domain</th>
|
2025-11-24 01:31:15 +00:00
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Provider</th>
|
|
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Services</th>
|
|
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Certificate</th>
|
2025-11-18 00:03:24 +00:00
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Expiry</th>
|
|
|
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
2025-11-24 01:31:15 +00:00
|
|
|
@for (domainView of domains(); track domainView.domain.id) {
|
|
|
|
|
<tr [class.opacity-50]="domainView.domain.isObsolete">
|
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
<div class="text-sm font-medium text-gray-900">{{ domainView.domain.domain }}</div>
|
|
|
|
|
@if (domainView.domain.isObsolete) {
|
|
|
|
|
<span class="text-xs text-red-600">Obsolete</span>
|
|
|
|
|
}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
@if (domainView.domain.dnsProvider === 'cloudflare') {
|
|
|
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
|
|
|
Cloudflare
|
|
|
|
|
</span>
|
|
|
|
|
} @else if (domainView.domain.dnsProvider === 'manual') {
|
|
|
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
|
|
|
Manual
|
|
|
|
|
</span>
|
|
|
|
|
} @else {
|
|
|
|
|
<span class="text-sm text-gray-400">None</span>
|
|
|
|
|
}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
|
|
{{ domainView.serviceCount }}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
@switch (domainView.certificateStatus) {
|
|
|
|
|
@case ('valid') {
|
|
|
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
|
|
|
Valid
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
@case ('expiring-soon') {
|
|
|
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
|
|
|
Expiring Soon
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
@case ('expired') {
|
|
|
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
|
|
|
Expired
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
@case ('pending') {
|
|
|
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
|
|
|
Pending
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
@default {
|
|
|
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
|
|
|
None
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</td>
|
2025-11-18 00:03:24 +00:00
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
2025-11-24 01:31:15 +00:00
|
|
|
@if (domainView.daysRemaining !== null) {
|
|
|
|
|
<span [ngClass]="domainView.daysRemaining <= 30 ? 'text-red-600 font-medium' : 'text-gray-500'">
|
|
|
|
|
{{ domainView.daysRemaining }} days
|
|
|
|
|
</span>
|
|
|
|
|
} @else {
|
|
|
|
|
<span class="text-gray-400">—</span>
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
</td>
|
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
|
2025-11-24 01:31:15 +00:00
|
|
|
<a
|
|
|
|
|
[routerLink]="['/domains', domainView.domain.domain]"
|
|
|
|
|
class="text-primary-600 hover:text-primary-900"
|
|
|
|
|
>
|
|
|
|
|
View Details
|
|
|
|
|
</a>
|
2025-11-18 00:03:24 +00:00
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
2025-11-24 01:31:15 +00:00
|
|
|
|
|
|
|
|
<!-- Summary Stats -->
|
|
|
|
|
<div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-4">
|
|
|
|
|
<div class="card">
|
|
|
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Total Domains</dt>
|
|
|
|
|
<dd class="mt-1 text-3xl font-semibold text-gray-900">{{ domains().length }}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card">
|
|
|
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Valid Certificates</dt>
|
|
|
|
|
<dd class="mt-1 text-3xl font-semibold text-green-600">{{ getStatusCount('valid') }}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card">
|
|
|
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Expiring Soon</dt>
|
|
|
|
|
<dd class="mt-1 text-3xl font-semibold text-yellow-600">{{ getStatusCount('expiring-soon') }}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card">
|
|
|
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Expired/Pending</dt>
|
|
|
|
|
<dd class="mt-1 text-3xl font-semibold text-red-600">{{ getStatusCount('expired') + getStatusCount('pending') }}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-18 00:03:24 +00:00
|
|
|
} @else {
|
|
|
|
|
<div class="card text-center py-12">
|
2025-11-24 01:31:15 +00:00
|
|
|
<p class="text-gray-500">No domains found</p>
|
|
|
|
|
<p class="text-sm text-gray-400 mt-2">
|
|
|
|
|
Sync your Cloudflare zones or manually add domains to get started
|
|
|
|
|
</p>
|
|
|
|
|
<button
|
|
|
|
|
(click)="syncDomains()"
|
|
|
|
|
class="mt-4 btn btn-primary"
|
|
|
|
|
>
|
|
|
|
|
Sync Cloudflare Domains
|
|
|
|
|
</button>
|
2025-11-18 00:03:24 +00:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
})
|
2025-11-24 01:31:15 +00:00
|
|
|
export class DomainsComponent implements OnInit {
|
2025-11-18 00:03:24 +00:00
|
|
|
private apiService = inject(ApiService);
|
2025-11-24 01:31:15 +00:00
|
|
|
private toastService = inject(ToastService);
|
|
|
|
|
|
|
|
|
|
domains = signal<DomainView[]>([]);
|
|
|
|
|
loading = signal(true);
|
|
|
|
|
syncing = signal(false);
|
2025-11-18 00:03:24 +00:00
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
2025-11-24 01:31:15 +00:00
|
|
|
this.loadDomains();
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
loadDomains(): void {
|
|
|
|
|
this.loading.set(true);
|
|
|
|
|
this.apiService.getDomains().subscribe({
|
2025-11-18 00:03:24 +00:00
|
|
|
next: (response) => {
|
|
|
|
|
if (response.success && response.data) {
|
2025-11-24 01:31:15 +00:00
|
|
|
this.domains.set(response.data);
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
2025-11-24 01:31:15 +00:00
|
|
|
this.loading.set(false);
|
|
|
|
|
},
|
|
|
|
|
error: () => {
|
|
|
|
|
this.loading.set(false);
|
2025-11-18 00:03:24 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
syncDomains(): void {
|
|
|
|
|
this.syncing.set(true);
|
|
|
|
|
this.apiService.syncCloudflareDomains().subscribe({
|
|
|
|
|
next: (response) => {
|
|
|
|
|
if (response.success) {
|
|
|
|
|
this.toastService.success('Cloudflare domains synced successfully');
|
|
|
|
|
this.loadDomains();
|
|
|
|
|
}
|
|
|
|
|
this.syncing.set(false);
|
|
|
|
|
},
|
|
|
|
|
error: (error) => {
|
|
|
|
|
this.toastService.error('Failed to sync Cloudflare domains: ' + (error.error?.error || error.message));
|
|
|
|
|
this.syncing.set(false);
|
2025-11-18 00:03:24 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
getStatusCount(status: string): number {
|
|
|
|
|
return this.domains().filter(d => d.certificateStatus === status).length;
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
}
|