mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
fe5c7eaa95
Build Docker images / Hub (push) Failing after 1m35s
Implement comprehensive domain data collection including provider detection (DNS, hosting, email, CA), HTTP headers, TLS certificate chains, and SEO metadata. Added PageSpeed Insights integration for monitors to track Core Web Vitals. - **hub**: - Add provider detection logic for DNS, email, and hosting. - Expand `Domain` entity to include SEO, headers, certificates, and enhanced registration details. - Implement automated collection of TLD, WHOIS raw data, and host country codes. - Update scheduler to track changes in providers and security settings (privacy/transfer lock). - Add PageSpeed check endpoint to monitor API. - **site**: - Update domain table and detail views to display new intelligence (providers, headers, SEO). - Implement PageSpeed metrics visualization with Core Web Vitals status indicators. - Add display options for provider information in the domain list. - **db**: - Add migration for new domain collection fields.
876 lines
28 KiB
Go
876 lines
28 KiB
Go
package domains
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/henrygd/beszel/internal/entities/domain"
|
|
"github.com/henrygd/beszel/internal/hub/domains/whois"
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/apis"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
)
|
|
|
|
// APIHandler handles domain API requests
|
|
type APIHandler struct {
|
|
app core.App
|
|
scheduler *Scheduler
|
|
}
|
|
|
|
// NewAPIHandler creates a new domain API handler
|
|
func NewAPIHandler(app core.App, scheduler *Scheduler) *APIHandler {
|
|
return &APIHandler{
|
|
app: app,
|
|
scheduler: scheduler,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers domain API routes
|
|
func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) {
|
|
api := se.Router.Group("/api/beszel/domains")
|
|
api.Bind(apis.RequireAuth())
|
|
|
|
api.GET("/", h.listDomains)
|
|
api.POST("/", h.createDomain)
|
|
api.POST("/lookup", h.lookupDomain)
|
|
api.GET("/{id}", h.getDomain)
|
|
api.PATCH("/{id}", h.updateDomain)
|
|
api.DELETE("/{id}", h.deleteDomain)
|
|
api.POST("/{id}/refresh", h.refreshDomain)
|
|
api.GET("/{id}/history", h.getDomainHistory)
|
|
api.GET("/{id}/stats", h.getDomainStats)
|
|
api.POST("/{id}/pause", h.pauseDomain)
|
|
api.POST("/{id}/resume", h.resumeDomain)
|
|
api.GET("/{id}/subdomains", h.getDomainSubdomains)
|
|
api.POST("/{id}/discover-subdomains", h.discoverSubdomains)
|
|
|
|
// Subdomain routes
|
|
api.DELETE("/subdomains/{subdomainId}", h.deleteSubdomain)
|
|
}
|
|
|
|
// listDomains lists all domains for the authenticated user
|
|
func (h *APIHandler) listDomains(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
records, err := h.app.FindAllRecords("domains",
|
|
dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}),
|
|
)
|
|
if err != nil {
|
|
return e.InternalServerError("failed to fetch domains", err)
|
|
}
|
|
|
|
domains := make([]map[string]interface{}, 0, len(records))
|
|
for _, record := range records {
|
|
domains = append(domains, h.recordToResponse(record))
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, domains)
|
|
}
|
|
|
|
// lookupDomain performs a WHOIS lookup without saving
|
|
func (h *APIHandler) lookupDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
var req struct {
|
|
DomainName string `json:"domain_name"`
|
|
}
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
if req.DomainName == "" {
|
|
return e.BadRequestError("domain_name is required", nil)
|
|
}
|
|
|
|
// Clean domain
|
|
domainName := cleanDomain(req.DomainName)
|
|
|
|
// Perform lookup
|
|
lookupSvc := whois.NewLookupService("")
|
|
ctx := e.Request.Context()
|
|
domainData, err := lookupSvc.LookupDomain(ctx, domainName)
|
|
if err != nil {
|
|
return e.InternalServerError("lookup failed", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, domainData)
|
|
}
|
|
|
|
// createDomain creates a new domain entry
|
|
func (h *APIHandler) createDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
var req struct {
|
|
DomainName string `json:"domain_name"`
|
|
AutoLookup bool `json:"auto_lookup"`
|
|
Tags []string `json:"tags"`
|
|
Notes string `json:"notes"`
|
|
PurchasePrice float64 `json:"purchase_price"`
|
|
CurrentValue float64 `json:"current_value"`
|
|
RenewalCost float64 `json:"renewal_cost"`
|
|
AutoRenew bool `json:"auto_renew"`
|
|
AlertDaysBefore int `json:"alert_days_before"`
|
|
SSLAlertEnabled bool `json:"ssl_alert_enabled"`
|
|
SSLAlertDays int `json:"ssl_alert_days"`
|
|
MonitorType string `json:"monitor_type"`
|
|
NotifyOnExpiry bool `json:"notify_on_expiry"`
|
|
NotifyOnSSL bool `json:"notify_on_ssl_expiry"`
|
|
NotifyOnDNS bool `json:"notify_on_dns_change"`
|
|
NotifyOnReg bool `json:"notify_on_registrar_change"`
|
|
// Manual expiry override when WHOIS fails
|
|
ExpiryDate string `json:"expiry_date,omitempty"`
|
|
CreationDate string `json:"creation_date,omitempty"`
|
|
}
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
if req.DomainName == "" {
|
|
return e.BadRequestError("domain_name is required", nil)
|
|
}
|
|
|
|
// Clean domain
|
|
domainName := cleanDomain(req.DomainName)
|
|
|
|
// Check if domain already exists for this user
|
|
existing, _ := h.app.FindFirstRecordByFilter("domains",
|
|
"domain_name = {:domain} && user = {:user}",
|
|
dbx.Params{"domain": domainName, "user": authRecord.Id})
|
|
if existing != nil {
|
|
return e.BadRequestError("domain already exists", nil)
|
|
}
|
|
|
|
collection, err := h.app.FindCollectionByNameOrId("domains")
|
|
if err != nil {
|
|
return e.InternalServerError("failed to find collection", err)
|
|
}
|
|
|
|
// Set defaults
|
|
if req.AlertDaysBefore <= 0 {
|
|
req.AlertDaysBefore = 30
|
|
}
|
|
|
|
record := core.NewRecord(collection)
|
|
record.Set("domain_name", domainName)
|
|
record.Set("status", domain.DomainStatusUnknown)
|
|
record.Set("active", true)
|
|
record.Set("tags", req.Tags)
|
|
record.Set("notes", req.Notes)
|
|
record.Set("purchase_price", req.PurchasePrice)
|
|
record.Set("current_value", req.CurrentValue)
|
|
record.Set("renewal_cost", req.RenewalCost)
|
|
record.Set("auto_renew", req.AutoRenew)
|
|
record.Set("alert_days_before", req.AlertDaysBefore)
|
|
record.Set("ssl_alert_enabled", req.SSLAlertEnabled)
|
|
record.Set("ssl_alert_days", req.SSLAlertDays)
|
|
record.Set("monitor_type", req.MonitorType)
|
|
record.Set("notify_on_expiry", req.NotifyOnExpiry)
|
|
record.Set("notify_on_ssl_expiry", req.NotifyOnSSL)
|
|
record.Set("notify_on_dns_change", req.NotifyOnDNS)
|
|
record.Set("notify_on_registrar_change", req.NotifyOnReg)
|
|
record.Set("user", authRecord.Id)
|
|
|
|
// Auto-lookup if requested
|
|
lookupHadExpiry := false
|
|
if req.AutoLookup {
|
|
lookupSvc := whois.NewLookupService("")
|
|
ctx := e.Request.Context()
|
|
domainData, err := lookupSvc.LookupDomain(ctx, domainName)
|
|
if err == nil && domainData != nil {
|
|
h.applyLookupData(record, domainData)
|
|
// Calculate status based on lookup results
|
|
status := domain.DomainStatusUnknown
|
|
if domainData.ExpiryDate != nil {
|
|
lookupHadExpiry = true
|
|
daysUntil := int(time.Until(*domainData.ExpiryDate).Hours() / 24)
|
|
if daysUntil < 0 {
|
|
status = domain.DomainStatusExpired
|
|
} else if daysUntil <= req.AlertDaysBefore {
|
|
status = domain.DomainStatusExpiring
|
|
} else {
|
|
status = domain.DomainStatusActive
|
|
}
|
|
} else if len(domainData.IPv4Addresses) > 0 || len(domainData.IPv6Addresses) > 0 || len(domainData.NameServers) > 0 {
|
|
// DNS resolves means the domain is active and functioning
|
|
status = domain.DomainStatusActive
|
|
}
|
|
record.Set("status", status)
|
|
}
|
|
}
|
|
|
|
// Apply manual expiry/creation dates if WHOIS didn't find them
|
|
if !lookupHadExpiry {
|
|
if req.ExpiryDate != "" {
|
|
if t, err := time.Parse("2006-01-02", req.ExpiryDate); err == nil {
|
|
record.Set("expiry_date", t)
|
|
// Recalculate status with manual expiry
|
|
daysUntil := int(time.Until(t).Hours() / 24)
|
|
status := domain.DomainStatusActive
|
|
if daysUntil < 0 {
|
|
status = domain.DomainStatusExpired
|
|
} else if daysUntil <= req.AlertDaysBefore {
|
|
status = domain.DomainStatusExpiring
|
|
}
|
|
record.Set("status", status)
|
|
}
|
|
}
|
|
if req.CreationDate != "" {
|
|
if t, err := time.Parse("2006-01-02", req.CreationDate); err == nil {
|
|
record.Set("creation_date", t)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to create domain", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusCreated, h.recordToResponse(record))
|
|
}
|
|
|
|
// getDomain gets a single domain
|
|
func (h *APIHandler) getDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
|
}
|
|
|
|
// updateDomain updates a domain
|
|
func (h *APIHandler) updateDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
var req map[string]interface{}
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
// Update allowed fields
|
|
if tags, ok := req["tags"]; ok {
|
|
record.Set("tags", tags)
|
|
}
|
|
if notes, ok := req["notes"]; ok {
|
|
record.Set("notes", notes)
|
|
}
|
|
if price, ok := req["purchase_price"]; ok {
|
|
record.Set("purchase_price", price)
|
|
}
|
|
if value, ok := req["current_value"]; ok {
|
|
record.Set("current_value", value)
|
|
}
|
|
if renewal, ok := req["renewal_cost"]; ok {
|
|
record.Set("renewal_cost", renewal)
|
|
}
|
|
if autoRenew, ok := req["auto_renew"]; ok {
|
|
record.Set("auto_renew", autoRenew)
|
|
}
|
|
if active, ok := req["active"]; ok {
|
|
record.Set("active", active)
|
|
}
|
|
if alertDays, ok := req["alert_days_before"]; ok {
|
|
record.Set("alert_days_before", alertDays)
|
|
}
|
|
if sslAlert, ok := req["ssl_alert_enabled"]; ok {
|
|
record.Set("ssl_alert_enabled", sslAlert)
|
|
}
|
|
for _, field := range []string{
|
|
"ssl_alert_days",
|
|
"monitor_type",
|
|
"notify_on_expiry",
|
|
"notify_on_ssl_expiry",
|
|
"notify_on_dns_change",
|
|
"notify_on_registrar_change",
|
|
"notify_on_value_change",
|
|
"value_change_threshold",
|
|
"quiet_hours_enabled",
|
|
"quiet_hours_start",
|
|
"quiet_hours_end",
|
|
} {
|
|
if value, ok := req[field]; ok {
|
|
record.Set(field, value)
|
|
}
|
|
}
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to update domain", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
|
}
|
|
|
|
// deleteDomain deletes a domain
|
|
func (h *APIHandler) deleteDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
if err := h.app.Delete(record); err != nil {
|
|
return e.InternalServerError("failed to delete domain", err)
|
|
}
|
|
|
|
return e.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
// refreshDomain triggers a manual refresh
|
|
func (h *APIHandler) refreshDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
if h.scheduler != nil {
|
|
if err := h.scheduler.RefreshDomain(id); err != nil {
|
|
return e.InternalServerError("failed to refresh domain", err)
|
|
}
|
|
}
|
|
|
|
updatedRecord, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.InternalServerError("failed to fetch refreshed domain", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, h.recordToResponse(updatedRecord))
|
|
}
|
|
|
|
// getDomainHistory gets the change history for a domain
|
|
func (h *APIHandler) getDomainHistory(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
|
|
// Verify domain ownership
|
|
domain, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
if domain.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
// Fetch history
|
|
records, err := h.app.FindAllRecords("domain_history",
|
|
dbx.NewExp("domain = {:domain}", dbx.Params{"domain": id}),
|
|
)
|
|
if err != nil {
|
|
return e.InternalServerError("failed to fetch history", err)
|
|
}
|
|
|
|
history := make([]map[string]interface{}, 0, len(records))
|
|
for _, record := range records {
|
|
history = append(history, map[string]interface{}{
|
|
"id": record.Id,
|
|
"change_type": record.GetString("change_type"),
|
|
"field_name": record.GetString("field_name"),
|
|
"old_value": record.GetString("old_value"),
|
|
"new_value": record.GetString("new_value"),
|
|
"created_at": record.GetDateTime("created_at").String(),
|
|
})
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, history)
|
|
}
|
|
|
|
// getDomainStats gets domain health statistics
|
|
func (h *APIHandler) getDomainStats(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
|
|
// Verify domain ownership
|
|
domain, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
if domain.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
// Calculate stats from domain history
|
|
stats := h.calculateDomainStats(id)
|
|
|
|
return e.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// calculateDomainStats calculates health statistics from domain history
|
|
func (h *APIHandler) calculateDomainStats(domainID string) map[string]interface{} {
|
|
// Get history for the last 30 days
|
|
since := time.Now().AddDate(0, 0, -30)
|
|
records, _ := h.app.FindRecordsByFilter(
|
|
"domain_history",
|
|
"domain = {:domain} && created_at >= {:since}",
|
|
"-created_at",
|
|
0, 0,
|
|
dbx.Params{
|
|
"domain": domainID,
|
|
"since": since.Format("2006-01-02 15:04:05"),
|
|
},
|
|
)
|
|
|
|
totalChanges := len(records)
|
|
expiryChanges := 0
|
|
sslChanges := 0
|
|
statusChanges := 0
|
|
|
|
for _, record := range records {
|
|
switch record.GetString("change_type") {
|
|
case "expiry":
|
|
expiryChanges++
|
|
case "ssl":
|
|
sslChanges++
|
|
case "status":
|
|
statusChanges++
|
|
}
|
|
}
|
|
|
|
// Get incidents count
|
|
incidentRecords, _ := h.app.FindRecordsByFilter(
|
|
"incidents",
|
|
"domain = {:domain}",
|
|
"-created",
|
|
0, 0,
|
|
dbx.Params{"domain": domainID},
|
|
)
|
|
|
|
return map[string]interface{}{
|
|
"total_changes": totalChanges,
|
|
"expiry_changes": expiryChanges,
|
|
"ssl_changes": sslChanges,
|
|
"status_changes": statusChanges,
|
|
"incidents_count": len(incidentRecords),
|
|
"period_days": 30,
|
|
}
|
|
}
|
|
|
|
// pauseDomain pauses domain monitoring
|
|
func (h *APIHandler) pauseDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
record.Set("active", false)
|
|
record.Set("status", "paused")
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to pause domain", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
|
}
|
|
|
|
// resumeDomain resumes domain monitoring
|
|
func (h *APIHandler) resumeDomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("domains", id)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
record.Set("active", true)
|
|
// Reset status - scheduler will update on next check
|
|
record.Set("status", "unknown")
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to resume domain", err)
|
|
}
|
|
|
|
// Trigger immediate refresh
|
|
if h.scheduler != nil {
|
|
h.scheduler.RefreshDomain(id)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
|
}
|
|
|
|
// recordToResponse converts a record to API response
|
|
func (h *APIHandler) recordToResponse(record *core.Record) map[string]interface{} {
|
|
expiryDate := record.GetDateTime("expiry_date").Time()
|
|
sslValidTo := record.GetDateTime("ssl_valid_to").Time()
|
|
|
|
// Calculate days until expiry
|
|
daysUntilExpiry := -1
|
|
if !expiryDate.IsZero() {
|
|
daysUntilExpiry = int(time.Until(expiryDate).Hours() / 24)
|
|
}
|
|
|
|
sslDaysUntil := -1
|
|
if !sslValidTo.IsZero() {
|
|
sslDaysUntil = int(time.Until(sslValidTo).Hours() / 24)
|
|
}
|
|
|
|
resp := map[string]interface{}{
|
|
"id": record.Id,
|
|
"domain_name": record.GetString("domain_name"),
|
|
"status": record.GetString("status"),
|
|
"active": record.GetBool("active"),
|
|
"days_until_expiry": daysUntilExpiry,
|
|
"registrar_name": record.GetString("registrar_name"),
|
|
"registrar_id": record.GetString("registrar_id"),
|
|
"registrar_url": record.GetString("registrar_url"),
|
|
"registry_domain_id": record.GetString("registry_domain_id"),
|
|
"dnssec": record.GetString("dnssec"),
|
|
"name_servers": record.Get("name_servers"),
|
|
"mx_records": record.Get("mx_records"),
|
|
"txt_records": record.Get("txt_records"),
|
|
"ipv4_addresses": record.Get("ipv4_addresses"),
|
|
"ipv6_addresses": record.Get("ipv6_addresses"),
|
|
"ssl_issuer": record.GetString("ssl_issuer"),
|
|
"ssl_issuer_country": record.GetString("ssl_issuer_country"),
|
|
"ssl_subject": record.GetString("ssl_subject"),
|
|
"ssl_days_until": sslDaysUntil,
|
|
"ssl_fingerprint": record.GetString("ssl_fingerprint"),
|
|
"ssl_key_size": record.GetInt("ssl_key_size"),
|
|
"ssl_signature_algo": record.GetString("ssl_signature_algo"),
|
|
"host_country": record.GetString("host_country"),
|
|
"host_region": record.GetString("host_region"),
|
|
"host_city": record.GetString("host_city"),
|
|
"host_isp": record.GetString("host_isp"),
|
|
"host_org": record.GetString("host_org"),
|
|
"host_as": record.GetString("host_as"),
|
|
"host_lat": record.GetFloat("host_lat"),
|
|
"host_lon": record.GetFloat("host_lon"),
|
|
"purchase_price": record.GetFloat("purchase_price"),
|
|
"current_value": record.GetFloat("current_value"),
|
|
"renewal_cost": record.GetFloat("renewal_cost"),
|
|
"auto_renew": record.GetBool("auto_renew"),
|
|
"alert_days_before": record.GetInt("alert_days_before"),
|
|
"ssl_alert_enabled": record.GetBool("ssl_alert_enabled"),
|
|
"ssl_alert_days": record.GetInt("ssl_alert_days"),
|
|
"monitor_type": record.GetString("monitor_type"),
|
|
"notify_on_expiry": record.GetBool("notify_on_expiry"),
|
|
"notify_on_ssl_expiry": record.GetBool("notify_on_ssl_expiry"),
|
|
"notify_on_dns_change": record.GetBool("notify_on_dns_change"),
|
|
"notify_on_registrar_change": record.GetBool("notify_on_registrar_change"),
|
|
"notify_on_value_change": record.GetBool("notify_on_value_change"),
|
|
"value_change_threshold": record.GetFloat("value_change_threshold"),
|
|
"quiet_hours_enabled": record.GetBool("quiet_hours_enabled"),
|
|
"quiet_hours_start": record.GetString("quiet_hours_start"),
|
|
"quiet_hours_end": record.GetString("quiet_hours_end"),
|
|
"registrant_name": record.GetString("registrant_name"),
|
|
"registrant_org": record.GetString("registrant_org"),
|
|
"registrant_country": record.GetString("registrant_country"),
|
|
"registrant_city": record.GetString("registrant_city"),
|
|
"registrant_state": record.GetString("registrant_state"),
|
|
"abuse_email": record.GetString("abuse_email"),
|
|
"abuse_phone": record.GetString("abuse_phone"),
|
|
"dns_provider": record.GetString("dns_provider"),
|
|
"hosting_provider": record.GetString("hosting_provider"),
|
|
"email_provider": record.GetString("email_provider"),
|
|
"ca_provider": record.GetString("ca_provider"),
|
|
"headers": record.Get("headers"),
|
|
"certificates": record.Get("certificates"),
|
|
"seo_meta": record.Get("seo_meta"),
|
|
"whois_raw": record.GetString("whois_raw"),
|
|
"privacy_enabled": record.GetBool("privacy_enabled"),
|
|
"transfer_lock": record.GetBool("transfer_lock"),
|
|
"tld": record.GetString("tld"),
|
|
"domain_statuses": record.Get("domain_statuses"),
|
|
"host_country_code": record.GetString("host_country_code"),
|
|
"tags": record.Get("tags"),
|
|
"notes": record.GetString("notes"),
|
|
"favicon_url": record.GetString("favicon_url"),
|
|
"created": record.GetDateTime("created").String(),
|
|
"updated": record.GetDateTime("updated").String(),
|
|
}
|
|
|
|
if !expiryDate.IsZero() {
|
|
resp["expiry_date"] = expiryDate.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
creationDate := record.GetDateTime("creation_date").Time()
|
|
if !creationDate.IsZero() {
|
|
resp["creation_date"] = creationDate.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
updatedDate := record.GetDateTime("updated_date").Time()
|
|
if !updatedDate.IsZero() {
|
|
resp["updated_date"] = updatedDate.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
sslValidFrom := record.GetDateTime("ssl_valid_from").Time()
|
|
if !sslValidFrom.IsZero() {
|
|
resp["ssl_valid_from"] = sslValidFrom.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if !sslValidTo.IsZero() {
|
|
resp["ssl_valid_to"] = sslValidTo.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
lastChecked := record.GetDateTime("last_checked").Time()
|
|
if !lastChecked.IsZero() {
|
|
resp["last_checked"] = lastChecked.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
func (h *APIHandler) applyLookupData(record *core.Record, domainData *domain.Domain) {
|
|
if domainData.ExpiryDate != nil {
|
|
record.Set("expiry_date", *domainData.ExpiryDate)
|
|
}
|
|
if domainData.CreationDate != nil {
|
|
record.Set("creation_date", *domainData.CreationDate)
|
|
}
|
|
if domainData.UpdatedDate != nil {
|
|
record.Set("updated_date", *domainData.UpdatedDate)
|
|
}
|
|
record.Set("registrar_name", domainData.RegistrarName)
|
|
record.Set("registrar_id", domainData.RegistrarID)
|
|
record.Set("registrar_url", domainData.RegistrarURL)
|
|
record.Set("registry_domain_id", domainData.RegistryDomainID)
|
|
record.Set("dnssec", domainData.DNSSEC)
|
|
record.Set("name_servers", domainData.NameServers)
|
|
record.Set("mx_records", domainData.MXRecords)
|
|
record.Set("txt_records", domainData.TXTRecords)
|
|
record.Set("ipv4_addresses", domainData.IPv4Addresses)
|
|
record.Set("ipv6_addresses", domainData.IPv6Addresses)
|
|
record.Set("ssl_issuer", domainData.SSLIssuer)
|
|
record.Set("ssl_issuer_country", domainData.SSLIssuerCountry)
|
|
record.Set("ssl_subject", domainData.SSLSubject)
|
|
if domainData.SSLValidFrom != nil {
|
|
record.Set("ssl_valid_from", *domainData.SSLValidFrom)
|
|
}
|
|
if domainData.SSLValidTo != nil {
|
|
record.Set("ssl_valid_to", *domainData.SSLValidTo)
|
|
}
|
|
record.Set("ssl_fingerprint", domainData.SSLFingerprint)
|
|
record.Set("ssl_key_size", domainData.SSLKeySize)
|
|
record.Set("ssl_signature_algo", domainData.SSLSignatureAlgo)
|
|
record.Set("host_country", domainData.HostCountry)
|
|
record.Set("host_region", domainData.HostRegion)
|
|
record.Set("host_city", domainData.HostCity)
|
|
record.Set("host_isp", domainData.HostISP)
|
|
record.Set("host_org", domainData.HostOrg)
|
|
record.Set("host_as", domainData.HostAS)
|
|
record.Set("host_lat", domainData.HostLat)
|
|
record.Set("host_lon", domainData.HostLon)
|
|
record.Set("registrant_name", domainData.RegistrantName)
|
|
record.Set("registrant_org", domainData.RegistrantOrg)
|
|
record.Set("registrant_street", domainData.RegistrantStreet)
|
|
record.Set("registrant_city", domainData.RegistrantCity)
|
|
record.Set("registrant_state", domainData.RegistrantState)
|
|
record.Set("registrant_country", domainData.RegistrantCountry)
|
|
record.Set("registrant_postal", domainData.RegistrantPostal)
|
|
record.Set("abuse_email", domainData.AbuseEmail)
|
|
record.Set("abuse_phone", domainData.AbusePhone)
|
|
record.Set("dns_provider", domainData.DNSProvider)
|
|
record.Set("hosting_provider", domainData.HostingProvider)
|
|
record.Set("email_provider", domainData.EmailProvider)
|
|
record.Set("ca_provider", domainData.CAProvider)
|
|
if len(domainData.Headers) > 0 {
|
|
record.Set("headers", domainData.Headers)
|
|
}
|
|
if len(domainData.Certificates) > 0 {
|
|
record.Set("certificates", domainData.Certificates)
|
|
}
|
|
if domainData.SEOMeta != nil {
|
|
record.Set("seo_meta", domainData.SEOMeta)
|
|
}
|
|
record.Set("whois_raw", domainData.WHOISRaw)
|
|
record.Set("privacy_enabled", domainData.PrivacyEnabled)
|
|
record.Set("transfer_lock", domainData.TransferLock)
|
|
record.Set("tld", domainData.TLD)
|
|
if len(domainData.DomainStatuses) > 0 {
|
|
record.Set("domain_statuses", domainData.DomainStatuses)
|
|
}
|
|
record.Set("host_country_code", domainData.HostCountryCode)
|
|
record.Set("favicon_url", domainData.FaviconURL)
|
|
record.Set("last_checked", time.Now())
|
|
}
|
|
|
|
// cleanDomain cleans and normalizes a domain name
|
|
func cleanDomain(domain string) string {
|
|
// Remove protocol
|
|
domain = strings.TrimPrefix(domain, "https://")
|
|
domain = strings.TrimPrefix(domain, "http://")
|
|
// Remove www prefix
|
|
domain = strings.TrimPrefix(domain, "www.")
|
|
// Remove path and query
|
|
if idx := strings.IndexAny(domain, "/?#"); idx != -1 {
|
|
domain = domain[:idx]
|
|
}
|
|
// Remove port
|
|
if idx := strings.Index(domain, ":"); idx != -1 {
|
|
domain = domain[:idx]
|
|
}
|
|
return strings.ToLower(strings.TrimSpace(domain))
|
|
}
|
|
|
|
// getDomainSubdomains returns all subdomains for a domain
|
|
func (h *APIHandler) getDomainSubdomains(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
domainID := e.Request.PathValue("id")
|
|
|
|
// Verify domain ownership
|
|
domain, err := h.app.FindRecordById("domains", domainID)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
if domain.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
records, err := h.app.FindAllRecords("subdomains",
|
|
dbx.NewExp("domain = {:domain}", dbx.Params{"domain": domainID}),
|
|
)
|
|
if err != nil {
|
|
return e.InternalServerError("failed to fetch subdomains", err)
|
|
}
|
|
|
|
subdomains := make([]map[string]interface{}, 0, len(records))
|
|
for _, record := range records {
|
|
subdomains = append(subdomains, map[string]interface{}{
|
|
"id": record.Id,
|
|
"domain": record.GetString("domain"),
|
|
"subdomain_name": record.GetString("subdomain_name"),
|
|
"full_domain": record.GetString("full_domain"),
|
|
"status": record.GetString("status"),
|
|
"ip_addresses": record.GetString("ip_addresses"),
|
|
"http_status": record.GetInt("http_status"),
|
|
"server_header": record.GetString("server_header"),
|
|
"discovery_source": record.GetString("discovery_source"),
|
|
"last_checked": record.GetDateTime("last_checked").Time(),
|
|
"created": record.GetDateTime("created").Time(),
|
|
"updated": record.GetDateTime("updated").Time(),
|
|
})
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, subdomains)
|
|
}
|
|
|
|
// discoverSubdomains triggers subdomain discovery for a domain
|
|
func (h *APIHandler) discoverSubdomains(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
domainID := e.Request.PathValue("id")
|
|
|
|
// Verify domain ownership
|
|
domain, err := h.app.FindRecordById("domains", domainID)
|
|
if err != nil {
|
|
return e.NotFoundError("domain not found", err)
|
|
}
|
|
if domain.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
// Trigger discovery asynchronously
|
|
go func() {
|
|
domainName := domain.GetString("domain_name")
|
|
userID := authRecord.Id
|
|
h.scheduler.discoverSubdomainsEnhanced(domain, domainName, userID)
|
|
}()
|
|
|
|
return e.JSON(http.StatusOK, map[string]string{"status": "discovery_started"})
|
|
}
|
|
|
|
// deleteSubdomain deletes a subdomain
|
|
func (h *APIHandler) deleteSubdomain(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
subdomainID := e.Request.PathValue("subdomainId")
|
|
|
|
// Get subdomain
|
|
subdomain, err := h.app.FindRecordById("subdomains", subdomainID)
|
|
if err != nil {
|
|
return e.NotFoundError("subdomain not found", err)
|
|
}
|
|
|
|
// Verify ownership
|
|
if subdomain.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
if err := h.app.Delete(subdomain); err != nil {
|
|
return e.InternalServerError("failed to delete subdomain", err)
|
|
}
|
|
|
|
return e.NoContent(http.StatusNoContent)
|
|
}
|