feat(domains): allow claiming username root domain
Build and publish Docker image / build-and-publish (push) Successful in 12s

This commit is contained in:
wfalkowski committed 2026-10-20 18:14:30 +00:00
1 parent 687f895345
commit 179a8c274b
1 file changed
+23 -8
+23 -8
View File
@@ -682,6 +682,7 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
const [customDomains, setCustomDomains] = useState([]); const [customDomains, setCustomDomains] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [newDomainName, setNewDomainName] = useState(''); const [newDomainName, setNewDomainName] = useState('');
const [useUsernameDomain, setUseUsernameDomain] = useState(false);
const [selectedServerForLink, setSelectedServerForLink] = useState(''); const [selectedServerForLink, setSelectedServerForLink] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [showLinkModal, setShowLinkModal] = useState(false); const [showLinkModal, setShowLinkModal] = useState(false);
@@ -765,6 +766,7 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
setSuccessMessage(data.message || 'Domain created successfully!'); setSuccessMessage(data.message || 'Domain created successfully!');
setShowCreateModal(false); setShowCreateModal(false);
setNewDomainName(''); setNewDomainName('');
setUseUsernameDomain(false);
setActionInProgress(false); setActionInProgress(false);
loadDomains(); loadDomains();
break; break;
@@ -837,19 +839,19 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
}, [loadDomains, loadCustomDomains]); }, [loadDomains, loadCustomDomains]);
const createDomain = () => { const createDomain = () => {
if (!newDomainName.trim()) { if (!useUsernameDomain && !newDomainName.trim()) {
setErrorMessage('Please enter a domain name'); setErrorMessage('Please enter a domain name');
return; return;
} }
// Validate domain name format // Validate domain name format
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
if (!domainRegex.test(newDomainName)) { if (!useUsernameDomain && !domainRegex.test(newDomainName)) {
setErrorMessage('Domain name can only contain letters, numbers, and hyphens. Cannot start or end with hyphen.'); setErrorMessage('Domain name can only contain letters, numbers, and hyphens. Cannot start or end with hyphen.');
return; return;
} }
if (newDomainName.length > 63) { if (!useUsernameDomain && newDomainName.length > 63) {
setErrorMessage('Domain name must be 63 characters or less'); setErrorMessage('Domain name must be 63 characters or less');
return; return;
} }
@@ -860,7 +862,8 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
sendMessage({ sendMessage({
type: 'create_domain', type: 'create_domain',
subdomain: newDomainName.trim().toLowerCase(), subdomain: useUsernameDomain ? '' : newDomainName.trim().toLowerCase(),
useUsernameDomain,
serverId: selectedServerForLink || null serverId: selectedServerForLink || null
}); });
}; };
@@ -1124,9 +1127,21 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
</button> </button>
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<SettingsPanel title="Domain Details" description={`Choose a name under u-${username}.${branding.domain}. Other names under your username are available too.`}> <SettingsPanel title="Domain Details" description={`A name such as "my-site" becomes my-site.u-${username}.${branding.domain}. You can also claim u-${username}.${branding.domain} directly.`}>
<div className="space-y-4"> <div className="space-y-4">
<div className="relative"> <label className="flex items-start gap-3 rounded-xl border border-dash-border bg-black/20 p-3 text-sm">
<input
type="checkbox"
checked={useUsernameDomain}
onChange={(event) => setUseUsernameDomain(event.target.checked)}
className="mt-1 accent-primary"
/>
<span>
<span className="block font-bold" style={{ color: colors.brightWhite }}>Use my username domain directly</span>
<span className="mt-1 block text-xs opacity-70">Creates <strong>u-{username}.{branding.domain}</strong> with no name in front.</span>
</span>
</label>
{!useUsernameDomain && <div className="relative">
<input <input
type="text" type="text"
value={newDomainName} value={newDomainName}
@@ -1138,7 +1153,7 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-black opacity-30 uppercase tracking-widest pointer-events-none"> <div className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-black opacity-30 uppercase tracking-widest pointer-events-none">
.u-{username}.{branding.domain} .u-{username}.{branding.domain}
</div> </div>
</div> </div>}
</div> </div>
</SettingsPanel> </SettingsPanel>
@@ -1153,7 +1168,7 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
<div className="flex gap-3"> <div className="flex gap-3">
<ActionButton onClick={() => setShowCreateModal(false)} variant="secondary" className="flex-1">Cancel</ActionButton> <ActionButton onClick={() => setShowCreateModal(false)} variant="secondary" className="flex-1">Cancel</ActionButton>
<ActionButton onClick={createDomain} variant="primary" className="flex-1" disabled={actionInProgress || !newDomainName.trim()}> <ActionButton onClick={createDomain} variant="primary" className="flex-1" disabled={actionInProgress || (!useUsernameDomain && !newDomainName.trim())}>
{actionInProgress ? <RefreshCw className="w-4 h-4 animate-spin" /> : 'Create Domain'} {actionInProgress ? <RefreshCw className="w-4 h-4 animate-spin" /> : 'Create Domain'}
</ActionButton> </ActionButton>
</div> </div>