diff --git a/api/src/services/platform_admin/realm_handler.py b/api/src/services/platform_admin/realm_handler.py index f910a44f..459a327e 100644 --- a/api/src/services/platform_admin/realm_handler.py +++ b/api/src/services/platform_admin/realm_handler.py @@ -41,6 +41,13 @@ def create_realm_in_keycloak( status_code=409, detail=f"Domain '{normalized_domain}' is already in use.", ) + + admin_parts = realm.adminEmail.strip().lower().split('@') + if len(admin_parts) != 2 or admin_parts[1] != normalized_domain: + raise HTTPException( + status_code=400, + detail=f"Admin email must belong to the '{normalized_domain}' domain.", + ) _ = self.admin.create_realm( realm_name=realm.name, diff --git a/web/src/components/AdminPanel.tsx b/web/src/components/AdminPanel.tsx index 442b5c0d..a362102d 100644 --- a/web/src/components/AdminPanel.tsx +++ b/web/src/components/AdminPanel.tsx @@ -3,6 +3,7 @@ import { useNavigate } from '@tanstack/react-router' import { TenantForm } from './admin/TenantForm' import { PreviewPanel } from './admin/PreviewPanel' import { getEmailDomain, isValidEmail } from '@/lib/emailValidation' +import { isValidTenantDomain, normalizeTenantDomain } from '@/lib/tenantDomainValidation' import { apiClient } from '../lib/api-client' import { toast } from 'sonner' @@ -51,7 +52,7 @@ export function CreateTenantPage() { e.preventDefault() setIsLoading(true) try { - const normalizedDomain = domain.trim().toLowerCase() + const normalizedDomain = normalizeTenantDomain(domain) const normalizedAdminEmail = adminEmail.trim().toLowerCase() const isAdminEmailFormatValid = isValidEmail(normalizedAdminEmail) const adminEmailDomain = getEmailDomain(normalizedAdminEmail) ?? '' @@ -59,6 +60,10 @@ export function CreateTenantPage() { toast.error('Domain is required.') return } + if (!isValidTenantDomain(normalizedDomain)) { + toast.error('Please provide a valid domain name.') + return + } if (!normalizedAdminEmail || !isAdminEmailFormatValid) { toast.error('Please provide a valid admin email.') return diff --git a/web/src/components/admin/TenantFormOrganization.tsx b/web/src/components/admin/TenantFormOrganization.tsx index 86bf92b3..0faadb6e 100644 --- a/web/src/components/admin/TenantFormOrganization.tsx +++ b/web/src/components/admin/TenantFormOrganization.tsx @@ -2,6 +2,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import RequiredAsterisk from '@/components/shared/RequiredAsterisk' import { getEmailDomain, isValidEmail } from '@/lib/emailValidation' +import { isValidTenantDomain, normalizeTenantDomain } from '@/lib/tenantDomainValidation' interface TenantFormOrganizationProps { realmName: string @@ -18,8 +19,9 @@ export function TenantFormOrganization({ adminEmail, setAdminEmail }: Readonly) { - const normalizedDomain = domain.trim().toLowerCase().replace(/^@/, '').replace(/^\*\./, '') + const normalizedDomain = normalizeTenantDomain(domain) const normalizedEmail = adminEmail.trim().toLowerCase() + const isDomainFormatValid = isValidTenantDomain(normalizedDomain) const isAdminEmailFormatValid = !!normalizedEmail && isValidEmail(normalizedEmail) const adminEmailDomain = getEmailDomain(normalizedEmail) ?? '' const isAdminEmailDomainMatching = @@ -48,9 +50,14 @@ export function TenantFormOrganization({ value={domain} onChange={(e) => setDomain(e.target.value)} className="h-10 bg-surface-subtle border-border" - aria-invalid={!normalizedDomain} + aria-invalid={!!domain.trim() && !isDomainFormatValid} required /> + {!!domain.trim() && !isDomainFormatValid && ( +

+ Please enter a valid domain name. +

+ )}
diff --git a/web/src/lib/tenantDomainValidation.ts b/web/src/lib/tenantDomainValidation.ts new file mode 100644 index 00000000..02754a01 --- /dev/null +++ b/web/src/lib/tenantDomainValidation.ts @@ -0,0 +1,20 @@ +const DOMAIN_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; + +export function normalizeTenantDomain(value: string): string { + return value.trim().toLowerCase().replace(/^@/, "").replace(/^\*\./, ""); +} + +export function isValidTenantDomain(value: string): boolean { + const domain = normalizeTenantDomain(value); + + if (!domain || domain.length > 253 || /\s/.test(domain) || domain.includes("..")) { + return false; + } + + const labels = domain.split("."); + if (labels.length < 2) { + return false; + } + + return labels.every((label) => DOMAIN_LABEL_RE.test(label)); +}