Compare commits
2 commits
83f0ae85d1
...
709bc2ca3a
| Author | SHA1 | Date | |
|---|---|---|---|
| 709bc2ca3a | |||
| b39fa1ecda |
12 changed files with 854 additions and 0 deletions
112
checks/domain-expiration.go
Normal file
112
checks/domain-expiration.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/pkg/domaininfo"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_WARNING_DAYS = 30
|
||||
DEFAULT_CRITICAL_DAYS = 7
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterChecker("domain-expiration", &DomainExpirationCheck{})
|
||||
}
|
||||
|
||||
type DomainExpirationCheck struct{}
|
||||
|
||||
func (p *DomainExpirationCheck) ID() string { return "domain-expiration" }
|
||||
func (p *DomainExpirationCheck) Name() string { return "Domain Expiration" }
|
||||
|
||||
func (p *DomainExpirationCheck) Availability() happydns.CheckerAvailability {
|
||||
return happydns.CheckerAvailability{ApplyToDomain: true}
|
||||
}
|
||||
|
||||
func (p *DomainExpirationCheck) Options() happydns.CheckerOptionsDocumentation {
|
||||
return happydns.CheckerOptionsDocumentation{
|
||||
RunOpts: []happydns.CheckerOptionDocumentation{
|
||||
{Id: "domainName", Type: "string", Label: "Domain name", AutoFill: happydns.AutoFillDomainName, Required: true},
|
||||
},
|
||||
UserOpts: []happydns.CheckerOptionDocumentation{
|
||||
{Id: "warningDays", Type: "number", Label: "Days before expiration to warn", Default: DEFAULT_WARNING_DAYS},
|
||||
{Id: "criticalDays", Type: "number", Label: "Days before expiration to alert", Default: DEFAULT_CRITICAL_DAYS},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DomainExpirationCheck) RunCheck(options happydns.CheckerOptions, meta map[string]string) (*happydns.CheckResult, error) {
|
||||
// 1. Extract domainName
|
||||
domainName, ok := options["domainName"].(string)
|
||||
if !ok || domainName == "" {
|
||||
return nil, fmt.Errorf("domainName is required")
|
||||
}
|
||||
domainName = strings.TrimSuffix(domainName, ".")
|
||||
|
||||
// 2. Extract thresholds (with defaults)
|
||||
warningDays := extractInt(options, "warningDays", DEFAULT_WARNING_DAYS)
|
||||
criticalDays := extractInt(options, "criticalDays", DEFAULT_CRITICAL_DAYS)
|
||||
|
||||
// 3. Try RDAP, fallback to WHOIS
|
||||
info, err := domaininfo.GetDomainRDAPInfo(happydns.Origin(domainName))
|
||||
if err != nil {
|
||||
info, err = domaininfo.GetDomainWhoisInfo(happydns.Origin(domainName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve domain info: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check expiration date presence
|
||||
if info.ExpirationDate == nil {
|
||||
return &happydns.CheckResult{
|
||||
Status: happydns.CheckResultStatusUnknown,
|
||||
StatusLine: "Expiration date not available",
|
||||
Report: info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 5. Compute days remaining
|
||||
daysUntil := int(math.Ceil(time.Until(*info.ExpirationDate).Hours() / 24))
|
||||
|
||||
// 6. Determine status
|
||||
var status happydns.CheckResultStatus
|
||||
var statusLine string
|
||||
switch {
|
||||
case daysUntil < 0:
|
||||
status = happydns.CheckResultStatusCritical
|
||||
statusLine = fmt.Sprintf("Domain expired %d day(s) ago (expired on %s)", -daysUntil, info.ExpirationDate.Format("2006-01-02"))
|
||||
case daysUntil < criticalDays:
|
||||
status = happydns.CheckResultStatusCritical
|
||||
statusLine = fmt.Sprintf("Domain expires in %d day(s) (on %s)", daysUntil, info.ExpirationDate.Format("2006-01-02"))
|
||||
case daysUntil < warningDays:
|
||||
status = happydns.CheckResultStatusWarn
|
||||
statusLine = fmt.Sprintf("Domain expires in %d day(s) (on %s)", daysUntil, info.ExpirationDate.Format("2006-01-02"))
|
||||
default:
|
||||
status = happydns.CheckResultStatusOK
|
||||
statusLine = fmt.Sprintf("Domain valid until %s (%d days)", info.ExpirationDate.Format("2006-01-02"), daysUntil)
|
||||
}
|
||||
|
||||
return &happydns.CheckResult{
|
||||
Status: status,
|
||||
StatusLine: statusLine,
|
||||
Report: info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractInt reads an int/float64 option with a default fallback.
|
||||
func extractInt(options happydns.CheckerOptions, key string, def int) int {
|
||||
if v, ok := options[key]; ok {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
case float64:
|
||||
return int(n)
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
30
web/src/lib/api/domaininfo.ts
Normal file
30
web/src/lib/api/domaininfo.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2022-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { postDomaininfoByDomain } from "$lib/api-base/sdk.gen";
|
||||
import type { DomainInfo } from "$lib/model/domaininfo";
|
||||
import { unwrapSdkResponse } from "./errors";
|
||||
|
||||
export async function getDomainInfo(domain: string): Promise<DomainInfo> {
|
||||
return unwrapSdkResponse(
|
||||
await postDomaininfoByDomain({ path: { domain } }),
|
||||
) as DomainInfo;
|
||||
}
|
||||
|
|
@ -137,6 +137,12 @@
|
|||
>
|
||||
{$t("menu.dns-resolver")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
active={page.route && page.route.id == "/whois/[[domain]]"}
|
||||
href="/whois"
|
||||
>
|
||||
{$t("menu.domain-info")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
active={page.route &&
|
||||
(page.route.id == "/checks" || page.route.id?.startsWith("/checks/"))}
|
||||
|
|
|
|||
|
|
@ -161,6 +161,40 @@
|
|||
"captcha": {
|
||||
"human-check": "Sind Sie ein Mensch? Bitte aktivieren Sie das untenstehende Kästchen, um fortzufahren:"
|
||||
},
|
||||
"domaininfo": {
|
||||
"page-title": "Domain-Informationen",
|
||||
"page-description": "Registrierungsinformationen für jeden Domainnamen nachschlagen",
|
||||
"domain-description": "Geben Sie den Domainnamen ein, den Sie nachschlagen möchten, z.B. {{domain}}.",
|
||||
"lookup": "Nachschlagen",
|
||||
"registrar": "Registrar",
|
||||
"registrar-url": "Registrar-Website",
|
||||
"nameservers": "Nameserver",
|
||||
"creation-date": "Registriert am",
|
||||
"expiration-date": "Läuft ab am",
|
||||
"status": "Domain-Status",
|
||||
"no-expiration": "Kein Ablaufdatum verfügbar",
|
||||
"no-creation": "Kein Registrierungsdatum verfügbar",
|
||||
"no-registrar": "Unbekannter Registrar",
|
||||
"expires-in": "Läuft in {{days}} Tagen ab",
|
||||
"expired": "Vor {{days}} Tagen abgelaufen",
|
||||
"expires-today": "Läuft heute ab",
|
||||
"domain-not-found": "Dieser Domainname scheint nicht registriert zu sein.",
|
||||
"error": "Domain-Informationen konnten nicht abgerufen werden.",
|
||||
"status-descriptions": {
|
||||
"ok": "Die Domain ist ohne Einschränkungen aktiv.",
|
||||
"active": "Die Domain ist aktiv.",
|
||||
"clientTransferProhibited": "Der Transfer zu einem anderen Registrar ist gesperrt.",
|
||||
"serverTransferProhibited": "Der Transfer ist durch die Registry verboten.",
|
||||
"clientUpdateProhibited": "Domain-Änderungen sind gesperrt.",
|
||||
"serverUpdateProhibited": "Domain-Änderungen sind durch die Registry verboten.",
|
||||
"clientDeleteProhibited": "Das Löschen der Domain ist gesperrt.",
|
||||
"serverDeleteProhibited": "Das Löschen der Domain ist durch die Registry verboten.",
|
||||
"clientHold": "Die Domain ist gesperrt (wird nicht aufgelöst).",
|
||||
"serverHold": "Die Domain ist durch die Registry gesperrt.",
|
||||
"pendingTransfer": "Ein Transfer zu einem anderen Registrar steht aus.",
|
||||
"pendingDelete": "Die Domain steht vor der Löschung."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "Seite nicht gefunden",
|
||||
|
|
@ -225,6 +259,7 @@
|
|||
"my-domains": "Meine Domains",
|
||||
"my-providers": "Meine Domain-Anbieter",
|
||||
"dns-resolver": "DNS-Resolver",
|
||||
"domain-info": "Domain-Informationen",
|
||||
"my-account": "Mein Konto",
|
||||
"logout": "Abmelden",
|
||||
"provider-features": "Unterstützte Anbieter",
|
||||
|
|
|
|||
|
|
@ -176,6 +176,40 @@
|
|||
"captcha": {
|
||||
"human-check": "Are you a human? Please check the box below to continue:"
|
||||
},
|
||||
"domaininfo": {
|
||||
"page-title": "Domain Information",
|
||||
"page-description": "Look up registration information for any domain name",
|
||||
"domain-description": "Enter the domain name you want to look up, e.g. {{domain}}.",
|
||||
"lookup": "Look up",
|
||||
"registrar": "Registrar",
|
||||
"registrar-url": "Registrar website",
|
||||
"nameservers": "Nameservers",
|
||||
"creation-date": "Registered on",
|
||||
"expiration-date": "Expires on",
|
||||
"status": "Domain status",
|
||||
"no-expiration": "No expiration date available",
|
||||
"no-creation": "No registration date available",
|
||||
"no-registrar": "Unknown registrar",
|
||||
"expires-in": "Expires in {{days}} days",
|
||||
"expired": "Expired {{days}} days ago",
|
||||
"expires-today": "Expires today",
|
||||
"domain-not-found": "This domain name does not appear to be registered.",
|
||||
"error": "Could not retrieve domain information.",
|
||||
"status-descriptions": {
|
||||
"ok": "The domain is active with no restrictions.",
|
||||
"active": "The domain is active.",
|
||||
"clientTransferProhibited": "Transfer to another registrar is locked.",
|
||||
"serverTransferProhibited": "Transfer is prohibited by the registry.",
|
||||
"clientUpdateProhibited": "Domain updates are locked.",
|
||||
"serverUpdateProhibited": "Domain updates are prohibited by the registry.",
|
||||
"clientDeleteProhibited": "Domain deletion is locked.",
|
||||
"serverDeleteProhibited": "Domain deletion is prohibited by the registry.",
|
||||
"clientHold": "The domain is on hold (not resolving).",
|
||||
"serverHold": "The domain is on hold by the registry.",
|
||||
"pendingTransfer": "A transfer to another registrar is pending.",
|
||||
"pendingDelete": "The domain is pending deletion."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "Page not found",
|
||||
|
|
@ -240,6 +274,7 @@
|
|||
"my-domains": "My domains",
|
||||
"my-providers": "My domain providers",
|
||||
"dns-resolver": "DNS resolver",
|
||||
"domain-info": "Domain Information",
|
||||
"checkers": "Domain Checkers",
|
||||
"my-account": "My account",
|
||||
"logout": "Sign out",
|
||||
|
|
|
|||
|
|
@ -161,6 +161,40 @@
|
|||
"captcha": {
|
||||
"human-check": "¿Eres humano? Por favor, marca la casilla de abajo para continuar:"
|
||||
},
|
||||
"domaininfo": {
|
||||
"page-title": "Información del dominio",
|
||||
"page-description": "Busca información de registro para cualquier nombre de dominio",
|
||||
"domain-description": "Introduce el nombre de dominio que quieres buscar, p.ej. {{domain}}.",
|
||||
"lookup": "Buscar",
|
||||
"registrar": "Registrador",
|
||||
"registrar-url": "Sitio web del registrador",
|
||||
"nameservers": "Servidores de nombres",
|
||||
"creation-date": "Registrado el",
|
||||
"expiration-date": "Expira el",
|
||||
"status": "Estado del dominio",
|
||||
"no-expiration": "No hay fecha de vencimiento disponible",
|
||||
"no-creation": "No hay fecha de registro disponible",
|
||||
"no-registrar": "Registrador desconocido",
|
||||
"expires-in": "Expira en {{days}} días",
|
||||
"expired": "Expiró hace {{days}} días",
|
||||
"expires-today": "Expira hoy",
|
||||
"domain-not-found": "Este nombre de dominio no parece estar registrado.",
|
||||
"error": "No se pudo recuperar la información del dominio.",
|
||||
"status-descriptions": {
|
||||
"ok": "El dominio está activo sin restricciones.",
|
||||
"active": "El dominio está activo.",
|
||||
"clientTransferProhibited": "La transferencia a otro registrador está bloqueada.",
|
||||
"serverTransferProhibited": "La transferencia está prohibida por el registro.",
|
||||
"clientUpdateProhibited": "Las actualizaciones del dominio están bloqueadas.",
|
||||
"serverUpdateProhibited": "Las actualizaciones del dominio están prohibidas por el registro.",
|
||||
"clientDeleteProhibited": "La eliminación del dominio está bloqueada.",
|
||||
"serverDeleteProhibited": "La eliminación del dominio está prohibida por el registro.",
|
||||
"clientHold": "El dominio está en espera (no resuelve).",
|
||||
"serverHold": "El dominio está en espera por el registro.",
|
||||
"pendingTransfer": "Hay una transferencia a otro registrador pendiente.",
|
||||
"pendingDelete": "El dominio está pendiente de eliminación."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "Página no encontrada",
|
||||
|
|
@ -225,6 +259,7 @@
|
|||
"my-domains": "Mis dominios",
|
||||
"my-providers": "Mis proveedores de dominios",
|
||||
"dns-resolver": "Resolvedor DNS",
|
||||
"domain-info": "Información del dominio",
|
||||
"my-account": "Mi cuenta",
|
||||
"logout": "Cerrar sesión",
|
||||
"provider-features": "Proveedores compatibles",
|
||||
|
|
|
|||
|
|
@ -154,6 +154,40 @@
|
|||
"captcha": {
|
||||
"human-check": "Êtes-vous un humain ? Veuillez cocher la case ci-dessous pour continuer :"
|
||||
},
|
||||
"domaininfo": {
|
||||
"page-title": "Informations sur le domaine",
|
||||
"page-description": "Recherchez les informations d'enregistrement pour n'importe quel nom de domaine",
|
||||
"domain-description": "Entrez le nom de domaine que vous souhaitez rechercher, par ex. {{domain}}.",
|
||||
"lookup": "Rechercher",
|
||||
"registrar": "Bureau d'enregistrement",
|
||||
"registrar-url": "Site du registrar",
|
||||
"nameservers": "Serveurs de noms",
|
||||
"creation-date": "Enregistré le",
|
||||
"expiration-date": "Expire le",
|
||||
"status": "Statut du domaine",
|
||||
"no-expiration": "Aucune date d'expiration disponible",
|
||||
"no-creation": "Aucune date d'enregistrement disponible",
|
||||
"no-registrar": "Registrar inconnu",
|
||||
"expires-in": "Expire dans {{days}} jours",
|
||||
"expired": "Expiré il y a {{days}} jours",
|
||||
"expires-today": "Expire aujourd'hui",
|
||||
"domain-not-found": "Ce nom de domaine ne semble pas être enregistré.",
|
||||
"error": "Impossible de récupérer les informations du domaine.",
|
||||
"status-descriptions": {
|
||||
"ok": "Le domaine est actif sans restrictions.",
|
||||
"active": "Le domaine est actif.",
|
||||
"clientTransferProhibited": "Le transfert vers un autre registrar est verrouillé.",
|
||||
"serverTransferProhibited": "Le transfert est interdit par le registre.",
|
||||
"clientUpdateProhibited": "Les modifications du domaine sont verrouillées.",
|
||||
"serverUpdateProhibited": "Les modifications du domaine sont interdites par le registre.",
|
||||
"clientDeleteProhibited": "La suppression du domaine est verrouillée.",
|
||||
"serverDeleteProhibited": "La suppression du domaine est interdite par le registre.",
|
||||
"clientHold": "Le domaine est suspendu (ne résout pas).",
|
||||
"serverHold": "Le domaine est suspendu par le registre.",
|
||||
"pendingTransfer": "Un transfert vers un autre registrar est en cours.",
|
||||
"pendingDelete": "Le domaine est en attente de suppression."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "Page introuvable",
|
||||
|
|
@ -217,6 +251,7 @@
|
|||
"menu": {
|
||||
"my-domains": "Mes domaines",
|
||||
"dns-resolver": "Résolveur DNS",
|
||||
"domain-info": "Informations sur le domaine",
|
||||
"my-account": "Mon compte",
|
||||
"logout": "Se déconnecter",
|
||||
"provider-features": "Fournisseurs supportés",
|
||||
|
|
|
|||
|
|
@ -161,6 +161,40 @@
|
|||
"captcha": {
|
||||
"human-check": "क्या आप इंसान हैं? जारी रखने के लिए कृपया नीचे दिया गया बॉक्स चेक करें:"
|
||||
},
|
||||
"domaininfo": {
|
||||
"page-title": "डोमेन जानकारी",
|
||||
"page-description": "किसी भी डोमेन नाम की पंजीकरण जानकारी देखें",
|
||||
"domain-description": "वह डोमेन नाम दर्ज करें जिसे आप देखना चाहते हैं, जैसे {{domain}}।",
|
||||
"lookup": "खोजें",
|
||||
"registrar": "रजिस्ट्रार",
|
||||
"registrar-url": "रजिस्ट्रार वेबसाइट",
|
||||
"nameservers": "नेमसर्वर",
|
||||
"creation-date": "पंजीकृत तिथि",
|
||||
"expiration-date": "समाप्ति तिथि",
|
||||
"status": "डोमेन स्थिति",
|
||||
"no-expiration": "समाप्ति तिथि उपलब्ध नहीं",
|
||||
"no-creation": "पंजीकरण तिथि उपलब्ध नहीं",
|
||||
"no-registrar": "अज्ञात रजिस्ट्रार",
|
||||
"expires-in": "{{days}} दिनों में समाप्त होगा",
|
||||
"expired": "{{days}} दिन पहले समाप्त हो गया",
|
||||
"expires-today": "आज समाप्त होगा",
|
||||
"domain-not-found": "यह डोमेन नाम पंजीकृत नहीं लगता।",
|
||||
"error": "डोमेन जानकारी प्राप्त नहीं हो सकी।",
|
||||
"status-descriptions": {
|
||||
"ok": "डोमेन बिना किसी प्रतिबंध के सक्रिय है।",
|
||||
"active": "डोमेन सक्रिय है।",
|
||||
"clientTransferProhibited": "किसी अन्य रजिस्ट्रार को स्थानांतरण लॉक है।",
|
||||
"serverTransferProhibited": "रजिस्ट्री द्वारा स्थानांतरण निषिद्ध है।",
|
||||
"clientUpdateProhibited": "डोमेन अपडेट लॉक हैं।",
|
||||
"serverUpdateProhibited": "रजिस्ट्री द्वारा डोमेन अपडेट निषिद्ध हैं।",
|
||||
"clientDeleteProhibited": "डोमेन हटाना लॉक है।",
|
||||
"serverDeleteProhibited": "रजिस्ट्री द्वारा डोमेन हटाना निषिद्ध है।",
|
||||
"clientHold": "डोमेन होल्ड पर है (रिज़ॉल्व नहीं हो रहा)।",
|
||||
"serverHold": "रजिस्ट्री द्वारा डोमेन होल्ड पर है।",
|
||||
"pendingTransfer": "किसी अन्य रजिस्ट्रार को स्थानांतरण लंबित है।",
|
||||
"pendingDelete": "डोमेन हटाने की प्रतीक्षा में है।"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "पृष्ठ नहीं मिला",
|
||||
|
|
@ -225,6 +259,7 @@
|
|||
"my-domains": "मेरे डोमेन",
|
||||
"my-providers": "मेरे डोमेन प्रदाता",
|
||||
"dns-resolver": "DNS रिज़ॉल्वर",
|
||||
"domain-info": "डोमेन जानकारी",
|
||||
"my-account": "मेरा खाता",
|
||||
"logout": "साइन आउट",
|
||||
"provider-features": "समर्थित प्रदाता",
|
||||
|
|
|
|||
|
|
@ -170,6 +170,40 @@
|
|||
"captcha": {
|
||||
"human-check": "您是人类吗?请勾选下方的框以继续:"
|
||||
},
|
||||
"domaininfo": {
|
||||
"page-title": "域名信息",
|
||||
"page-description": "查询任意域名的注册信息",
|
||||
"domain-description": "输入您要查询的域名,例如 {{domain}}。",
|
||||
"lookup": "查询",
|
||||
"registrar": "注册商",
|
||||
"registrar-url": "注册商网站",
|
||||
"nameservers": "域名服务器",
|
||||
"creation-date": "注册日期",
|
||||
"expiration-date": "到期日期",
|
||||
"status": "域名状态",
|
||||
"no-expiration": "无到期日期信息",
|
||||
"no-creation": "无注册日期信息",
|
||||
"no-registrar": "未知注册商",
|
||||
"expires-in": "{{days}} 天后到期",
|
||||
"expired": "已于 {{days}} 天前到期",
|
||||
"expires-today": "今日到期",
|
||||
"domain-not-found": "该域名似乎未被注册。",
|
||||
"error": "无法获取域名信息。",
|
||||
"status-descriptions": {
|
||||
"ok": "域名处于活跃状态,无任何限制。",
|
||||
"active": "域名处于活跃状态。",
|
||||
"clientTransferProhibited": "转移至其他注册商的功能已锁定。",
|
||||
"serverTransferProhibited": "注册局禁止转移。",
|
||||
"clientUpdateProhibited": "域名更新功能已锁定。",
|
||||
"serverUpdateProhibited": "注册局禁止域名更新。",
|
||||
"clientDeleteProhibited": "域名删除功能已锁定。",
|
||||
"serverDeleteProhibited": "注册局禁止删除域名。",
|
||||
"clientHold": "域名已暂停(无法解析)。",
|
||||
"serverHold": "注册局已暂停该域名。",
|
||||
"pendingTransfer": "正在等待转移至其他注册商。",
|
||||
"pendingDelete": "域名等待删除中。"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "页面未找到",
|
||||
|
|
@ -234,6 +268,7 @@
|
|||
"my-domains": "我的域名",
|
||||
"my-providers": "我的提供商",
|
||||
"dns-resolver": "DNS解析器",
|
||||
"domain-info": "域名信息",
|
||||
"my-account": "个人中心",
|
||||
"logout": "退出",
|
||||
"provider-features": "支持的提供商",
|
||||
|
|
|
|||
24
web/src/lib/model/domaininfo.ts
Normal file
24
web/src/lib/model/domaininfo.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2022-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import type { HappydnsDomainInfo } from "$lib/api-base/types.gen";
|
||||
|
||||
export type DomainInfo = HappydnsDomainInfo;
|
||||
444
web/src/routes/whois/[[domain]]/+page.svelte
Normal file
444
web/src/routes/whois/[[domain]]/+page.svelte
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
<!--
|
||||
This file is part of the happyDomain (R) project.
|
||||
Copyright (c) 2022-2026 happyDomain
|
||||
Authors: Pierre-Olivier Mercier, et al.
|
||||
|
||||
This program is offered under a commercial and under the AGPL license.
|
||||
For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
|
||||
For AGPL licensing:
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { untrack } from "svelte";
|
||||
import { preventDefault } from "svelte/legacy";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Col,
|
||||
Container,
|
||||
FormGroup,
|
||||
Input,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Row,
|
||||
Spinner,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { getDomainInfo } from "$lib/api/domaininfo";
|
||||
import type { DomainInfo } from "$lib/model/domaininfo";
|
||||
import { domains } from "$lib/stores/domains";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
interface Props {
|
||||
data: { domain?: string };
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let domain = $derived(data.domain ?? "");
|
||||
let inputDomain = $state("");
|
||||
|
||||
let error_response: string | null = $state(null);
|
||||
let not_found = $state(false);
|
||||
let request_pending = $state(false);
|
||||
let info: DomainInfo | null = $state(null);
|
||||
|
||||
function fetchInfo(d: string) {
|
||||
if (!d) return;
|
||||
|
||||
request_pending = true;
|
||||
error_response = null;
|
||||
not_found = false;
|
||||
info = null;
|
||||
|
||||
getDomainInfo(d).then(
|
||||
(result) => {
|
||||
info = result;
|
||||
request_pending = false;
|
||||
},
|
||||
(error: unknown) => {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("doesn't exist")) {
|
||||
not_found = true;
|
||||
} else {
|
||||
error_response = msg;
|
||||
}
|
||||
request_pending = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (domain) {
|
||||
untrack(() => {
|
||||
inputDomain = domain;
|
||||
fetchInfo(domain);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function submit() {
|
||||
if (!inputDomain) return;
|
||||
|
||||
if (inputDomain === domain) {
|
||||
fetchInfo(inputDomain);
|
||||
} else {
|
||||
navigate("/whois/" + encodeURIComponent(inputDomain), { noScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
function statusColor(code: string): string {
|
||||
const lc = code.toLowerCase();
|
||||
if (lc === "ok" || lc === "active") return "success";
|
||||
if (lc.includes("hold")) return "danger";
|
||||
if (lc.includes("prohibited") || lc.includes("pending")) return "info";
|
||||
return "secondary";
|
||||
}
|
||||
|
||||
function expirationProgress(expiration?: string): number {
|
||||
if (!expiration) return 0;
|
||||
const days = daysUntilExpiration(expiration);
|
||||
if (days <= 0) return 0;
|
||||
return Math.min(100, Math.round((days / 365) * 100));
|
||||
}
|
||||
|
||||
function expirationColor(expiration?: string): string {
|
||||
if (!expiration) return "secondary";
|
||||
const days = Math.round((new Date(expiration).getTime() - Date.now()) / 86400000);
|
||||
if (days < 0) return "danger";
|
||||
if (days < 30) return "danger";
|
||||
if (days < 90) return "warning";
|
||||
return "success";
|
||||
}
|
||||
|
||||
function daysUntilExpiration(expiration?: string): number {
|
||||
if (!expiration) return 0;
|
||||
return Math.round((new Date(expiration).getTime() - Date.now()) / 86400000);
|
||||
}
|
||||
|
||||
function formatDate(iso?: string): string {
|
||||
if (!iso) return "";
|
||||
return new Date(iso).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{$t("domaininfo.page-title")}
|
||||
{domain ? domain : ""}
|
||||
- happyDomain
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if domain}
|
||||
<Container fluid class="flex-fill d-flex flex-column">
|
||||
<Row class="flex-grow-1">
|
||||
<Col md={{ offset: 0, size: 4 }} class="bg-light pt-3 pb-5">
|
||||
<div class="sticky-top">
|
||||
<div class="mb-4">
|
||||
<h1 class="display-6 fw-bold">
|
||||
{$t("domaininfo.page-title")}
|
||||
</h1>
|
||||
</div>
|
||||
<form onsubmit={preventDefault(submit)}>
|
||||
<FormGroup>
|
||||
<label for="domain-input">
|
||||
{$t("common.domain")}
|
||||
</label>
|
||||
<Input
|
||||
id="domain-input"
|
||||
class="font-monospace"
|
||||
list="my-domains"
|
||||
required
|
||||
placeholder="example.com"
|
||||
bind:value={inputDomain}
|
||||
/>
|
||||
<div class="form-text">
|
||||
{@html $t("domaininfo.domain-description", {
|
||||
domain: `<span class="font-monospace">example.com</span>`,
|
||||
})}
|
||||
</div>
|
||||
<datalist id="my-domains">
|
||||
{#if $domains}
|
||||
{#each $domains as dn (dn.id)}
|
||||
<option>{dn.domain}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</datalist>
|
||||
</FormGroup>
|
||||
<div class="mx-3 d-flex justify-content-end">
|
||||
<Button type="submit" color="primary" disabled={request_pending}>
|
||||
{#if request_pending}
|
||||
<Spinner size="sm" />
|
||||
{/if}
|
||||
{$t("domaininfo.lookup")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{#if request_pending}
|
||||
<Col md="8" class="pt-5 pb-5 d-flex align-items-center justify-content-center">
|
||||
<div class="text-center text-muted">
|
||||
<Spinner />
|
||||
<p class="mt-3">{$t("common.spinning")}…</p>
|
||||
</div>
|
||||
</Col>
|
||||
{:else if not_found}
|
||||
<Col md="8" class="pt-3 pb-5">
|
||||
<h2 class="display-7 fw-bold mt-3">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
{domain}
|
||||
</h2>
|
||||
<div class="card border-warning mt-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-exclamation-triangle text-warning fs-3 me-3"></i>
|
||||
<p class="card-text mb-0">{$t("domaininfo.domain-not-found")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
{:else if error_response !== null}
|
||||
<Col md="8" class="pt-3 pb-5">
|
||||
<h2 class="display-7 fw-bold mt-3">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
{$t("domaininfo.error")}
|
||||
</h2>
|
||||
<div class="card border-danger mt-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-x-circle text-danger fs-3 me-3"></i>
|
||||
<p class="card-text mb-0">{error_response}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
{:else if info !== null}
|
||||
<Col md="8" class="pt-3 pb-5">
|
||||
<h2 class="display-7 fw-bold mt-3 mb-1 font-monospace">
|
||||
{info.name ?? domain}
|
||||
</h2>
|
||||
|
||||
<!-- Status badges -->
|
||||
{#if info.status && info.status.length > 0}
|
||||
<div class="mb-4">
|
||||
<p class="text-muted small mb-1">{$t("domaininfo.status")}</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{#each info.status as code}
|
||||
<Badge
|
||||
color={statusColor(code)}
|
||||
title={$t(`domaininfo.status-descriptions.${code}`) || code}
|
||||
>
|
||||
{code}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{#if info.status.length === 1}
|
||||
{@const desc = $t(
|
||||
`domaininfo.status-descriptions.${info.status[0]}`,
|
||||
)}
|
||||
{#if desc && !desc.startsWith("domaininfo.")}
|
||||
<p class="text-muted small mt-1 mb-0">{desc}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Dates card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<Row>
|
||||
<Col sm="6" class="mb-3 mb-sm-0">
|
||||
<p class="text-muted small mb-1">
|
||||
<i class="bi bi-calendar-check me-1"></i>
|
||||
{$t("domaininfo.creation-date")}
|
||||
</p>
|
||||
{#if info.creation}
|
||||
<p class="fw-semibold mb-0">{formatDate(info.creation)}</p>
|
||||
{:else}
|
||||
<p class="text-muted mb-0">
|
||||
{$t("domaininfo.no-creation")}
|
||||
</p>
|
||||
{/if}
|
||||
</Col>
|
||||
<Col sm="6">
|
||||
<p class="text-muted small mb-1">
|
||||
<i class="bi bi-calendar-x me-1"></i>
|
||||
{$t("domaininfo.expiration-date")}
|
||||
</p>
|
||||
{#if info.expiration}
|
||||
{@const days = daysUntilExpiration(info.expiration)}
|
||||
{@const color = expirationColor(info.expiration)}
|
||||
{@const expiresLabel = days < 0 ? $t("domaininfo.expired", { days: Math.abs(days) }) : days === 0 ? $t("domaininfo.expires-today") : $t("domaininfo.expires-in", { days })}
|
||||
<p class="fw-semibold mb-1">
|
||||
{formatDate(info.expiration)}
|
||||
</p>
|
||||
<div
|
||||
class="progress mb-1"
|
||||
style="height: 6px;"
|
||||
title={expiresLabel}
|
||||
>
|
||||
<div
|
||||
class="progress-bar bg-{color}"
|
||||
role="progressbar"
|
||||
style="width: {expirationProgress(info.expiration)}%"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-{color} small mb-0">
|
||||
{#if days < 0}
|
||||
{$t("domaininfo.expired", { days: Math.abs(days) })}
|
||||
{:else if days === 0}
|
||||
{$t("domaininfo.expires-today")}
|
||||
{:else}
|
||||
{$t("domaininfo.expires-in", { days })}
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted mb-0">
|
||||
{$t("domaininfo.no-expiration")}
|
||||
</p>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<!-- Registrar card -->
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle class="h6 mb-0 fw-bold">
|
||||
<i class="bi bi-building me-1"></i>
|
||||
{$t("domaininfo.registrar")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if info.registrar}
|
||||
<p class="fw-semibold mb-1">{info.registrar}</p>
|
||||
{#if info.registrar_url}
|
||||
<a
|
||||
href={info.registrar_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
>
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>
|
||||
{$t("domaininfo.registrar-url")}
|
||||
</a>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-muted mb-0">
|
||||
{$t("domaininfo.no-registrar")}
|
||||
</p>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- Nameservers card -->
|
||||
{#if info.nameservers && info.nameservers.length > 0}
|
||||
<Col md={6}>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle class="h6 mb-0 fw-bold">
|
||||
<i class="bi bi-hdd-network me-1"></i>
|
||||
{$t("domaininfo.nameservers")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<ListGroup flush>
|
||||
{#each info.nameservers as ns}
|
||||
<ListGroupItem class="font-monospace py-2"
|
||||
>{ns}</ListGroupItem
|
||||
>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
</Container>
|
||||
{:else}
|
||||
<div class="my-5 container flex-fill">
|
||||
<div class="text-center">
|
||||
<h1 class="display-6 fw-bold">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
{$t("domaininfo.page-title")}
|
||||
</h1>
|
||||
<p class="lead mt-1">
|
||||
{$t("domaininfo.page-description")}
|
||||
</p>
|
||||
</div>
|
||||
<Row class="justify-content-center mt-4">
|
||||
<Col md="10" lg="8">
|
||||
<div class="card rounded-4 p-2">
|
||||
<div class="card-body">
|
||||
<form onsubmit={preventDefault(submit)}>
|
||||
<FormGroup>
|
||||
<label for="domain-input-landing">
|
||||
{$t("common.domain")}
|
||||
</label>
|
||||
<Input
|
||||
id="domain-input-landing"
|
||||
class="font-monospace"
|
||||
list="my-domains-landing"
|
||||
required
|
||||
placeholder="example.com"
|
||||
bind:value={inputDomain}
|
||||
/>
|
||||
<div class="form-text">
|
||||
{@html $t("domaininfo.domain-description", {
|
||||
domain: `<span class="font-monospace">example.com</span>`,
|
||||
})}
|
||||
</div>
|
||||
<datalist id="my-domains-landing">
|
||||
{#if $domains}
|
||||
{#each $domains as dn (dn.id)}
|
||||
<option>{dn.domain}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</datalist>
|
||||
</FormGroup>
|
||||
<div class="d-flex justify-content-end">
|
||||
<Button type="submit" color="primary" disabled={request_pending}>
|
||||
{#if request_pending}
|
||||
<Spinner size="sm" />
|
||||
{/if}
|
||||
{$t("domaininfo.lookup")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
{/if}
|
||||
28
web/src/routes/whois/[[domain]]/+page.ts
Normal file
28
web/src/routes/whois/[[domain]]/+page.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2022-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import type { Load } from "@sveltejs/kit";
|
||||
|
||||
export const load: Load = async ({ params }) => {
|
||||
return {
|
||||
domain: params.domain,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue