mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
@@ -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(
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user