improvement(platform): landing page cleanup, MX cache fixes, and auth util extraction (#3683)

* fix(enterprise): remove dead variables resourceLabel, CHECK_PATH, allFeatures, RESOURCE_TYPE_LABEL

* fix: cap MX cache size, deduplicate validateCallbackUrl, add slug duplicate guard

* revert: remove slug duplicate guard

* refactor: extract validateCallbackUrl to shared util, evict stale MX cache entries on lookup

* refactor: move validateCallbackUrl into input-validation.ts

* fix: guard validateCallbackUrl against server-side window, skip eviction on cache update

* fix(auth): remove redundant validateCallbackUrl re-check on already-safe callbackUrl

* chore(auth): add comment explaining why safeCallbackUrl skip re-validation

* chore: remove redundant inline comments
This commit is contained in:
Waleed
2026-03-19 14:02:35 -07:00
committed by GitHub
parent 30b7192e75
commit 413c45d863
5 changed files with 44 additions and 69 deletions

View File

@@ -16,6 +16,7 @@ import {
} from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
@@ -53,24 +54,6 @@ const PASSWORD_VALIDATIONS = {
},
}
const validateCallbackUrl = (url: string): boolean => {
try {
if (url.startsWith('/')) {
return true
}
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
if (url.startsWith(currentOrigin)) {
return true
}
return false
} catch (error) {
logger.error('Error validating callback URL:', { error, url })
return false
}
}
const validatePassword = (passwordValue: string): string[] => {
const errors: string[] = []
@@ -106,13 +89,13 @@ export default function LoginPage({
const buttonClass = useBrandedButtonClass()
const callbackUrlParam = searchParams?.get('callbackUrl')
const isValidCallbackUrl = callbackUrlParam ? validateCallbackUrl(callbackUrlParam) : false
const invalidCallbackRef = useRef(false)
if (callbackUrlParam && !validateCallbackUrl(callbackUrlParam) && !invalidCallbackRef.current) {
if (callbackUrlParam && !isValidCallbackUrl && !invalidCallbackRef.current) {
invalidCallbackRef.current = true
logger.warn('Invalid callback URL detected and blocked:', { url: callbackUrlParam })
}
const callbackUrl =
callbackUrlParam && validateCallbackUrl(callbackUrlParam) ? callbackUrlParam : '/workspace'
const callbackUrl = isValidCallbackUrl ? callbackUrlParam! : '/workspace'
const isInviteFlow = searchParams?.get('invite_flow') === 'true'
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
@@ -192,7 +175,7 @@ export default function LoginPage({
}
try {
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
const safeCallbackUrl = callbackUrl
let errorHandled = false
const result = await client.signIn.email(

View File

@@ -35,25 +35,6 @@ const ACTOR_COLORS: Record<string, string> = {
/** Left accent bar opacity by recency — newest is brightest. */
const ACCENT_OPACITIES = [0.75, 0.5, 0.35, 0.22, 0.12, 0.05] as const
/** Human-readable label per resource type. */
const RESOURCE_TYPE_LABEL: Record<string, string> = {
workflow: 'Workflow',
member: 'Member',
byok_key: 'BYOK Key',
api_key: 'API Key',
permission_group: 'Permission Group',
credential_set: 'Credential Set',
knowledge_base: 'Knowledge Base',
environment: 'Environment',
mcp_server: 'MCP Server',
file: 'File',
webhook: 'Webhook',
chat: 'Chat',
table: 'Table',
folder: 'Folder',
document: 'Document',
}
interface LogEntry {
id: number
actor: string
@@ -189,7 +170,6 @@ function AuditRow({ entry, index }: AuditRowProps) {
const color = ACTOR_COLORS[entry.actor] ?? '#F6F6F6'
const accentOpacity = ACCENT_OPACITIES[index] ?? 0.04
const timeAgo = formatTimeAgo(entry.insertedAt)
const resourceLabel = RESOURCE_TYPE_LABEL[entry.resourceType]
return (
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
@@ -292,9 +272,6 @@ function AuditLogPreview() {
)
}
const CHECK_PATH =
'M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z'
interface PermissionFeature {
name: string
key: string
@@ -377,8 +354,6 @@ function AccessControlPanel() {
const isInView = useInView(ref, { once: true, margin: '-40px' })
const [accessState, setAccessState] = useState<Record<string, boolean>>(INITIAL_ACCESS_STATE)
const allFeatures = PERMISSION_CATEGORIES.flatMap((c) => c.features)
return (
<div ref={ref}>
<div className='lg:hidden'>

View File

@@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from 'next/navigation'
import { Button, Input, Label } from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import { env, isFalsy } from '@/lib/core/config/env'
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
@@ -29,24 +30,6 @@ const validateEmailField = (emailValue: string): string[] => {
return errors
}
const validateCallbackUrl = (url: string): boolean => {
try {
if (url.startsWith('/')) {
return true
}
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
if (url.startsWith(currentOrigin)) {
return true
}
return false
} catch (error) {
logger.error('Error validating callback URL:', { error, url })
return false
}
}
export default function SSOForm() {
const router = useRouter()
const searchParams = useSearchParams()
@@ -115,7 +98,7 @@ export default function SSOForm() {
}
try {
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
const safeCallbackUrl = callbackUrl
await client.signIn.sso({
email: emailValue,

View File

@@ -1169,3 +1169,26 @@ export function validatePaginationCursor(
return { isValid: true, sanitized: value }
}
/**
* Validates a callback URL to prevent open redirect attacks.
* Accepts relative paths and absolute URLs matching the current origin.
*
* @param url - The callback URL to validate
* @returns true if the URL is safe to redirect to
*/
export function validateCallbackUrl(url: string): boolean {
try {
if (url.startsWith('/')) return true
if (typeof window === 'undefined') return false
const currentOrigin = window.location.origin
if (url.startsWith(currentOrigin)) return true
return false
} catch (error) {
logger.error('Error validating callback URL:', { error, url })
return false
}
}

View File

@@ -64,6 +64,14 @@ const DISPOSABLE_MX_BACKENDS = new Set(['in.mail.gw', 'smtp.catchmail.io', 'mx.y
/** Per-domain MX result cache — avoids redundant DNS queries for concurrent or repeated sign-ups */
const mxCache = new Map<string, { result: boolean; expires: number }>()
const MX_CACHE_MAX = 1_000
function setMxCache(domain: string, entry: { result: boolean; expires: number }) {
if (mxCache.size >= MX_CACHE_MAX && !mxCache.has(domain)) {
mxCache.delete(mxCache.keys().next().value!)
}
mxCache.set(domain, entry)
}
/**
* Validates email syntax using RFC 5322 compliant regex
@@ -124,7 +132,10 @@ export async function isDisposableMxBackend(email: string): Promise<boolean> {
const now = Date.now()
const cached = mxCache.get(domain)
if (cached && cached.expires > now) return cached.result
if (cached) {
if (cached.expires > now) return cached.result
mxCache.delete(domain)
}
let timeoutId: ReturnType<typeof setTimeout> | undefined
try {
@@ -135,10 +146,10 @@ export async function isDisposableMxBackend(email: string): Promise<boolean> {
}
)
const result = await Promise.race([mxCheckPromise, timeoutPromise])
mxCache.set(domain, { result: result.isDisposableBackend, expires: now + 5 * 60 * 1000 })
setMxCache(domain, { result: result.isDisposableBackend, expires: now + 5 * 60 * 1000 })
return result.isDisposableBackend
} catch {
mxCache.set(domain, { result: false, expires: now + 60 * 1000 })
setMxCache(domain, { result: false, expires: now + 60 * 1000 })
return false
} finally {
clearTimeout(timeoutId)