feat(domains): support username root domain claims
Build and publish Docker image / build-and-publish (push) Successful in 9s

This commit is contained in:
wfalkowski committed 2026-10-20 18:14:30 +00:00
1 parent 2768f5f97d
commit 079140ce49
4 files changed
+19 -14

No files matched your search

+11 -6
View File
@@ -463,7 +463,7 @@ class DomainManager:
""" """
try: try:
# Validate inputs # Validate inputs
if not username or not subdomain_name: if not username or subdomain_name is None:
return { return {
'success': False, 'success': False,
'message': "Username and subdomain name are required", 'message': "Username and subdomain name are required",
@@ -478,7 +478,7 @@ class DomainManager:
return {'success': False, 'message': "Username cannot be represented as a DNS name", 'domain_data': None} return {'success': False, 'message': "Username cannot be represented as a DNS name", 'domain_data': None}
# Hostnames are single labels below u-<username>. # Hostnames are single labels below u-<username>.
if not self._is_valid_subdomain(subdomain_name): if subdomain_name and not self._is_valid_subdomain(subdomain_name):
return { return {
'success': False, 'success': False,
'message': "Invalid subdomain name. Use only letters, numbers, and hyphens", 'message': "Invalid subdomain name. Use only letters, numbers, and hyphens",
@@ -487,7 +487,7 @@ class DomainManager:
# Construct domains # Construct domains
user_domain = f"u-{username}.{self.base_domain}" user_domain = f"u-{username}.{self.base_domain}"
full_subdomain = f"{subdomain_name}.{user_domain}" full_subdomain = f"{subdomain_name}.{user_domain}" if subdomain_name else user_domain
namespace = self.ensure_user_namespace(username) namespace = self.ensure_user_namespace(username)
if not namespace['success']: if not namespace['success']:
return {'success': False, 'message': namespace['message'], 'domain_data': None} return {'success': False, 'message': namespace['message'], 'domain_data': None}
@@ -631,7 +631,7 @@ class DomainManager:
def delete_subdomain(self, username: str, subdomain_name: str) -> Dict: def delete_subdomain(self, username: str, subdomain_name: str) -> Dict:
"""Delete a subdomain""" """Delete a subdomain"""
try: try:
if not username or not subdomain_name: if not username or subdomain_name is None:
return { return {
'success': False, 'success': False,
'message': "Username and subdomain name are required" 'message': "Username and subdomain name are required"
@@ -639,9 +639,14 @@ class DomainManager:
username = username.lower().strip() username = username.lower().strip()
subdomain_name = subdomain_name.lower().strip() subdomain_name = subdomain_name.lower().strip()
if not self._is_valid_username_label(username) or not self._is_valid_subdomain(subdomain_name): if not self._is_valid_username_label(username):
return {'success': False, 'message': "Invalid username or subdomain"} return {'success': False, 'message': "Invalid username or subdomain"}
user_domain = f"u-{username}.{self.base_domain}" user_domain = f"u-{username}.{self.base_domain}"
# Releasing the optional root claim must never remove the shared namespace DNS.
if not subdomain_name:
return {'success': True, 'message': f"Domain claim {user_domain} deleted"}
if not self._is_valid_subdomain(subdomain_name):
return {'success': False, 'message': "Invalid username or subdomain"}
full_subdomain = f"{subdomain_name}.{user_domain}" full_subdomain = f"{subdomain_name}.{user_domain}"
# New namespaces use a shared wildcard in the parent zone. Do not # New namespaces use a shared wildcard in the parent zone. Do not
@@ -848,7 +853,7 @@ class DomainManager:
subdomain_name = subdomain_name.lower().strip() subdomain_name = subdomain_name.lower().strip()
# First validate the subdomain name # First validate the subdomain name
if not self._is_valid_subdomain(subdomain_name): if subdomain_name and not self._is_valid_subdomain(subdomain_name):
return { return {
'available': False, 'available': False,
'message': f"Invalid subdomain name: {subdomain_name}" 'message': f"Invalid subdomain name: {subdomain_name}"
+2 -2
View File
@@ -113,10 +113,10 @@ def create_domain_api():
return jsonify({'error': 'Domain service not available'}), 503 return jsonify({'error': 'Domain service not available'}), 503
data = request.get_json() data = request.get_json()
subdomain = data.get('subdomain') subdomain = '' if data.get('useUsernameDomain') is True else data.get('subdomain')
server_id = data.get('serverId') server_id = data.get('serverId')
if not subdomain: if subdomain is None:
return jsonify({'error': 'Subdomain name required'}), 400 return jsonify({'error': 'Subdomain name required'}), 400
# Check if subdomain is available # Check if subdomain is available
+2 -2
View File
@@ -258,9 +258,9 @@ def apply_plan(api: DeSec, namespaces: Iterable[str], copies: List[Dict],
UPDATE user_domains UPDATE user_domains
SET record_type = ?, ip_address = ?, ttl = 3600 SET record_type = ?, ip_address = ?, ttl = 3600
WHERE COALESCE(is_custom, FALSE) = FALSE WHERE COALESCE(is_custom, FALSE) = FALSE
AND lower(full_domain) LIKE ? ESCAPE '!' AND (lower(full_domain) = ? OR lower(full_domain) LIKE ? ESCAPE '!')
""", """,
('CNAME', api.base_domain, pattern), ('CNAME', api.base_domain, f'{namespace}.{api.base_domain}', pattern),
) )
connection.commit() connection.commit()
+4 -4
View File
@@ -1383,12 +1383,12 @@ async def handle_create_domain(client_id, data):
user_id = client_info['user_id'] user_id = client_info['user_id']
username = client_info['username'] username = client_info['username']
subdomain_name = data.get('subdomain') subdomain_name = '' if data.get('useUsernameDomain') is True else data.get('subdomain')
server_id = data.get('serverId') server_id = data.get('serverId')
logger.info(f"DEBUG: Creating domain for user {username}, subdomain: {subdomain_name}") logger.info(f"DEBUG: Creating domain for user {username}, subdomain: {subdomain_name}")
if not subdomain_name: if subdomain_name is None:
await send_message(client_id, {'type': 'error', 'error': 'Subdomain name required'}) await send_message(client_id, {'type': 'error', 'error': 'Subdomain name required'})
return return
@@ -1398,7 +1398,7 @@ async def handle_create_domain(client_id, data):
return return
# Validate subdomain name # Validate subdomain name
if not domain_manager._is_valid_subdomain(subdomain_name): if subdomain_name and not domain_manager._is_valid_subdomain(subdomain_name):
await send_message(client_id, {'type': 'error', 'error': 'Invalid subdomain name. Use only lowercase letters, numbers, and hyphens (1-63 chars)'}) await send_message(client_id, {'type': 'error', 'error': 'Invalid subdomain name. Use only lowercase letters, numbers, and hyphens (1-63 chars)'})
return return
@@ -1409,7 +1409,7 @@ async def handle_create_domain(client_id, data):
try: try:
# Check if subdomain is available (improved check) # Check if subdomain is available (improved check)
user_domain = f"u-{username}.{domain_manager.base_domain}" user_domain = f"u-{username}.{domain_manager.base_domain}"
full_subdomain = f"{subdomain_name}.{user_domain}" full_subdomain = f"{subdomain_name}.{user_domain}" if subdomain_name else user_domain
# Check if domain already exists in database # Check if domain already exists in database
with database_connection() as conn: with database_connection() as conn: