Refactor registrant config and clean up duplicate endpoints
- Use REGISTRANT_* env vars instead of DEFAULT_* for registrant info - Add REGISTRANT_ADDRESS2 to env config - Remove duplicate /childns/* endpoints (keep /glue/* only) - Add error handling to glue record endpoints - Document /domains/register endpoint in README Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,12 @@ NAMECHEAP_SANDBOX=true
|
|||||||
# REST API Server Auth
|
# REST API Server Auth
|
||||||
API_SERVER_KEY=your_api_server_key
|
API_SERVER_KEY=your_api_server_key
|
||||||
|
|
||||||
# Registrant Info
|
# Registrant Info (default for domain registration)
|
||||||
REGISTRANT_ORGANIZATION=
|
REGISTRANT_ORGANIZATION=
|
||||||
REGISTRANT_FIRST_NAME=
|
REGISTRANT_FIRST_NAME=
|
||||||
REGISTRANT_LAST_NAME=
|
REGISTRANT_LAST_NAME=
|
||||||
REGISTRANT_ADDRESS1=
|
REGISTRANT_ADDRESS1=
|
||||||
|
REGISTRANT_ADDRESS2=
|
||||||
REGISTRANT_CITY=
|
REGISTRANT_CITY=
|
||||||
REGISTRANT_STATE_PROVINCE=
|
REGISTRANT_STATE_PROVINCE=
|
||||||
REGISTRANT_POSTAL_CODE=
|
REGISTRANT_POSTAL_CODE=
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ Environment variables in `.env` (copy from `.env.example`):
|
|||||||
- `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY`, `NAMECHEAP_USERNAME`, `NAMECHEAP_CLIENT_IP` - Namecheap API credentials
|
- `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY`, `NAMECHEAP_USERNAME`, `NAMECHEAP_CLIENT_IP` - Namecheap API credentials
|
||||||
- `NAMECHEAP_SANDBOX` - Use sandbox API (true/false)
|
- `NAMECHEAP_SANDBOX` - Use sandbox API (true/false)
|
||||||
- `API_SERVER_KEY` - REST API authentication key (stored in Vault: `secret/namecheap/api-server`)
|
- `API_SERVER_KEY` - REST API authentication key (stored in Vault: `secret/namecheap/api-server`)
|
||||||
|
- `REGISTRANT_*` - Default registrant info for domain registration (ORGANIZATION, FIRST_NAME, LAST_NAME, ADDRESS1, ADDRESS2, CITY, STATE_PROVINCE, POSTAL_CODE, COUNTRY, PHONE, EMAIL)
|
||||||
|
|
||||||
### systemd Services
|
### systemd Services
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ NAMECHEAP_SANDBOX=false
|
|||||||
# REST API 서버 인증
|
# REST API 서버 인증
|
||||||
API_SERVER_KEY=your_api_server_key # Vault: secret/namecheap/api-server
|
API_SERVER_KEY=your_api_server_key # Vault: secret/namecheap/api-server
|
||||||
|
|
||||||
# 등록자 정보
|
# 등록자 정보 (도메인 등록 시 기본값)
|
||||||
REGISTRANT_ORGANIZATION=회사명
|
REGISTRANT_ORGANIZATION=회사명
|
||||||
REGISTRANT_FIRST_NAME=이름
|
REGISTRANT_FIRST_NAME=이름
|
||||||
REGISTRANT_LAST_NAME=성
|
REGISTRANT_LAST_NAME=성
|
||||||
REGISTRANT_ADDRESS1=주소
|
REGISTRANT_ADDRESS1=주소1
|
||||||
|
REGISTRANT_ADDRESS2=주소2 (선택)
|
||||||
REGISTRANT_CITY=도시
|
REGISTRANT_CITY=도시
|
||||||
REGISTRANT_STATE_PROVINCE=시/도
|
REGISTRANT_STATE_PROVINCE=시/도
|
||||||
REGISTRANT_POSTAL_CODE=우편번호
|
REGISTRANT_POSTAL_CODE=우편번호
|
||||||
@@ -286,6 +287,7 @@ journalctl --user -u namecheap-api.service -f
|
|||||||
|--------|----------|------|
|
|--------|----------|------|
|
||||||
| GET | `/domains` | 도메인 목록 |
|
| GET | `/domains` | 도메인 목록 |
|
||||||
| POST | `/domains/check` | 도메인 가용성 확인 |
|
| POST | `/domains/check` | 도메인 가용성 확인 |
|
||||||
|
| POST | `/domains/register` | 도메인 등록 (과금됨) |
|
||||||
| GET | `/domains/{domain}` | 도메인 상세 정보 |
|
| GET | `/domains/{domain}` | 도메인 상세 정보 |
|
||||||
| POST | `/domains/{domain}/renew` | 도메인 갱신 |
|
| POST | `/domains/{domain}/renew` | 도메인 갱신 |
|
||||||
| GET | `/domains/{domain}/contacts` | 등록자 정보 조회 |
|
| GET | `/domains/{domain}/contacts` | 등록자 정보 조회 |
|
||||||
|
|||||||
122
api_server.py
122
api_server.py
@@ -190,41 +190,57 @@ def get_dns_records(domain: str):
|
|||||||
@app.post("/dns/{domain}/glue")
|
@app.post("/dns/{domain}/glue")
|
||||||
def create_glue_record(domain: str, req: GlueRecordRequest):
|
def create_glue_record(domain: str, req: GlueRecordRequest):
|
||||||
"""Create a glue record (child nameserver) for a domain"""
|
"""Create a glue record (child nameserver) for a domain"""
|
||||||
|
from namecheap import NamecheapError
|
||||||
parts = domain.rsplit(".", 1)
|
parts = domain.rsplit(".", 1)
|
||||||
if len(parts) != 2:
|
if len(parts) != 2:
|
||||||
raise HTTPException(status_code=400, detail="Invalid domain format")
|
raise HTTPException(status_code=400, detail="Invalid domain format")
|
||||||
sld, tld = parts
|
sld, tld = parts
|
||||||
return api.ns_create(sld, tld, req.nameserver, req.ip)
|
try:
|
||||||
|
return api.ns_create(sld, tld, req.nameserver, req.ip)
|
||||||
|
except NamecheapError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.get("/dns/{domain}/glue/{nameserver}")
|
@app.get("/dns/{domain}/glue/{nameserver}")
|
||||||
def get_glue_record(domain: str, nameserver: str):
|
def get_glue_record(domain: str, nameserver: str):
|
||||||
"""Get info about a glue record (child nameserver)"""
|
"""Get info about a glue record (child nameserver)"""
|
||||||
|
from namecheap import NamecheapError
|
||||||
parts = domain.rsplit(".", 1)
|
parts = domain.rsplit(".", 1)
|
||||||
if len(parts) != 2:
|
if len(parts) != 2:
|
||||||
raise HTTPException(status_code=400, detail="Invalid domain format")
|
raise HTTPException(status_code=400, detail="Invalid domain format")
|
||||||
sld, tld = parts
|
sld, tld = parts
|
||||||
return api.ns_get_info(sld, tld, nameserver)
|
try:
|
||||||
|
return api.ns_get_info(sld, tld, nameserver)
|
||||||
|
except NamecheapError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.put("/dns/{domain}/glue")
|
@app.put("/dns/{domain}/glue")
|
||||||
def update_glue_record(domain: str, req: GlueRecordUpdateRequest):
|
def update_glue_record(domain: str, req: GlueRecordUpdateRequest):
|
||||||
"""Update a glue record (child nameserver) IP address"""
|
"""Update a glue record (child nameserver) IP address"""
|
||||||
|
from namecheap import NamecheapError
|
||||||
parts = domain.rsplit(".", 1)
|
parts = domain.rsplit(".", 1)
|
||||||
if len(parts) != 2:
|
if len(parts) != 2:
|
||||||
raise HTTPException(status_code=400, detail="Invalid domain format")
|
raise HTTPException(status_code=400, detail="Invalid domain format")
|
||||||
sld, tld = parts
|
sld, tld = parts
|
||||||
return api.ns_update(sld, tld, req.nameserver, req.old_ip, req.ip)
|
try:
|
||||||
|
return api.ns_update(sld, tld, req.nameserver, req.old_ip, req.ip)
|
||||||
|
except NamecheapError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/dns/{domain}/glue/{nameserver}")
|
@app.delete("/dns/{domain}/glue/{nameserver}")
|
||||||
def delete_glue_record(domain: str, nameserver: str):
|
def delete_glue_record(domain: str, nameserver: str):
|
||||||
"""Delete a glue record (child nameserver)"""
|
"""Delete a glue record (child nameserver)"""
|
||||||
|
from namecheap import NamecheapError
|
||||||
parts = domain.rsplit(".", 1)
|
parts = domain.rsplit(".", 1)
|
||||||
if len(parts) != 2:
|
if len(parts) != 2:
|
||||||
raise HTTPException(status_code=400, detail="Invalid domain format")
|
raise HTTPException(status_code=400, detail="Invalid domain format")
|
||||||
sld, tld = parts
|
sld, tld = parts
|
||||||
return api.ns_delete(sld, tld, nameserver)
|
try:
|
||||||
|
return api.ns_delete(sld, tld, nameserver)
|
||||||
|
except NamecheapError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
# ===== Account Endpoints =====
|
# ===== Account Endpoints =====
|
||||||
@@ -341,12 +357,110 @@ def get_openai_schema():
|
|||||||
"tld": {"type": "string", "description": "TLD like com, net, io"}
|
"tld": {"type": "string", "description": "TLD like com, net, io"}
|
||||||
},
|
},
|
||||||
"required": ["tld"]
|
"required": ["tld"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "register_domain",
|
||||||
|
"description": "Register a new domain. WARNING: This will charge your account.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {"type": "string", "description": "Domain to register (e.g., example.com)"},
|
||||||
|
"years": {"type": "integer", "default": 1, "description": "Registration period (1-10 years)"}
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Domain Registration =====
|
||||||
|
|
||||||
|
class DomainRegisterRequest(BaseModel):
|
||||||
|
domain: str
|
||||||
|
years: int = 1
|
||||||
|
add_whois_guard: bool = True
|
||||||
|
# Registrant info (optional - uses default if not provided)
|
||||||
|
first_name: Optional[str] = None
|
||||||
|
last_name: Optional[str] = None
|
||||||
|
organization: Optional[str] = None
|
||||||
|
address1: Optional[str] = None
|
||||||
|
address2: Optional[str] = None
|
||||||
|
city: Optional[str] = None
|
||||||
|
state_province: Optional[str] = None
|
||||||
|
postal_code: Optional[str] = None
|
||||||
|
country: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Default registrant info from environment
|
||||||
|
def get_default_registrant() -> RegistrantInfo:
|
||||||
|
return RegistrantInfo(
|
||||||
|
first_name=os.getenv("REGISTRANT_FIRST_NAME", ""),
|
||||||
|
last_name=os.getenv("REGISTRANT_LAST_NAME", ""),
|
||||||
|
organization=os.getenv("REGISTRANT_ORGANIZATION", ""),
|
||||||
|
address1=os.getenv("REGISTRANT_ADDRESS1", ""),
|
||||||
|
address2=os.getenv("REGISTRANT_ADDRESS2", ""),
|
||||||
|
city=os.getenv("REGISTRANT_CITY", ""),
|
||||||
|
state_province=os.getenv("REGISTRANT_STATE_PROVINCE", ""),
|
||||||
|
postal_code=os.getenv("REGISTRANT_POSTAL_CODE", ""),
|
||||||
|
country=os.getenv("REGISTRANT_COUNTRY", "JP"),
|
||||||
|
phone=os.getenv("REGISTRANT_PHONE", ""),
|
||||||
|
email=os.getenv("REGISTRANT_EMAIL", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/domains/register")
|
||||||
|
def register_domain(req: DomainRegisterRequest):
|
||||||
|
"""
|
||||||
|
Register a new domain. WARNING: This will charge your account.
|
||||||
|
|
||||||
|
If registrant info is not provided, uses default from environment.
|
||||||
|
"""
|
||||||
|
from namecheap import NamecheapError
|
||||||
|
|
||||||
|
# Use provided info or fall back to defaults
|
||||||
|
default = get_default_registrant()
|
||||||
|
registrant = RegistrantInfo(
|
||||||
|
first_name=req.first_name or default.first_name,
|
||||||
|
last_name=req.last_name or default.last_name,
|
||||||
|
organization=req.organization or default.organization,
|
||||||
|
address1=req.address1 or default.address1,
|
||||||
|
address2=req.address2 or default.address2,
|
||||||
|
city=req.city or default.city,
|
||||||
|
state_province=req.state_province or default.state_province,
|
||||||
|
postal_code=req.postal_code or default.postal_code,
|
||||||
|
country=req.country or default.country,
|
||||||
|
phone=req.phone or default.phone,
|
||||||
|
email=req.email or default.email,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
required = ["first_name", "last_name", "address1", "city", "state_province",
|
||||||
|
"postal_code", "country", "phone", "email"]
|
||||||
|
missing = [f for f in required if not getattr(registrant, f)]
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Missing required registrant fields: {', '.join(missing)}. Set DEFAULT_* env vars or provide in request."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = api.domains_create(
|
||||||
|
domain=req.domain,
|
||||||
|
registrant=registrant,
|
||||||
|
years=req.years,
|
||||||
|
add_whois_guard=req.add_whois_guard,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except NamecheapError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
Reference in New Issue
Block a user